diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3682efb9f5..46ab6491ef 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) @@ -121,6 +121,8 @@ dependencies { implementation(projects.features.send.impl) implementation(projects.features.qrScanning.api) implementation(projects.features.qrScanning.impl) + implementation(projects.features.staking.api) + implementation(projects.features.staking.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -183,7 +185,6 @@ dependencies { implementation(deps.reKotlin) implementation(deps.zxing.qrCore) implementation(deps.coil) - implementation(deps.appsflyer) implementation(deps.amplitude) implementation(deps.kotsonGson) implementation(deps.spongecastle.core) @@ -216,9 +217,12 @@ dependencies { androidTestImplementation(deps.test.kaspresso.compose) androidTestImplementation(deps.test.compose.junit) androidTestImplementation(deps.test.hamcrest) + androidTestImplementation(deps.test.hilt) + kaptAndroidTest(deps.test.hilt.compiler) /** Chucker */ debugImplementation(deps.chucker) + mockedImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt new file mode 100644 index 0000000000..e1c23e8e9b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt @@ -0,0 +1,28 @@ +package com.tangem.common + +import androidx.test.core.app.ApplicationProvider +import com.tangem.tap.ApplicationEntryPoint +import com.tangem.tap.TangemApplication +import dagger.hilt.android.testing.OnComponentReadyRunner +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class ApplicationInjectionExecutionRule : TestRule { + + private val tangemApplication: TangemApplication + get() = ApplicationProvider.getApplicationContext() + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + OnComponentReadyRunner.addListener( + tangemApplication, ApplicationEntryPoint::class.java + ) { _: ApplicationEntryPoint -> + tangemApplication.init() + } + base.evaluate() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt new file mode 100644 index 0000000000..4b3278bc9f --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -0,0 +1,56 @@ +package com.tangem.common + +import android.Manifest +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.espresso.intent.Intents +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.rule.GrantPermissionRule +import com.kaspersky.components.composesupport.config.withComposeSupport +import com.kaspersky.kaspresso.kaspresso.Kaspresso +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import com.tangem.tap.MainActivity +import com.tangem.tap.domain.sdk.TangemSdkManager +import dagger.hilt.android.testing.HiltAndroidRule +import org.junit.Rule +import org.junit.rules.RuleChain +import org.junit.runner.RunWith +import javax.inject.Inject + +@RunWith(AndroidJUnit4::class) +abstract class BaseTestCase : TestCase( + kaspressoBuilder = Kaspresso.Builder.withComposeSupport() +) { + + @Inject + lateinit var tangemSdkManager: TangemSdkManager + + @get:Rule + open val composeTestRule = createAndroidComposeRule() + + @get:Rule + val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( + Manifest.permission.POST_NOTIFICATIONS, + Manifest.permission.CAMERA + ) + + private val hiltRule = HiltAndroidRule(this) + + @Rule + @JvmField + val ruleChain = RuleChain + .outerRule(hiltRule) + .around(ApplicationInjectionExecutionRule()) + + protected fun setupHooks( + additionalBeforeSection: () -> Unit = {}, + additionalAfterSection: () -> Unit = {}, + ) = before { + hiltRule.inject() + Intents.init() + additionalBeforeSection() + }.after { + additionalAfterSection() + Intents.release() + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt b/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt new file mode 100644 index 0000000000..3f5c02d5a5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/HiltTestRunner.kt @@ -0,0 +1,17 @@ +package com.tangem.common + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner +import com.tangem.common.di.TangemMockedApplication_Application + +class HiltTestRunner : AndroidJUnitRunner() { + + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + return super.newApplication(cl, TangemMockedApplication_Application::class.java.name, context) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/TangemEmptyApplication.kt b/app/src/androidTest/kotlin/com/tangem/common/TangemEmptyApplication.kt new file mode 100644 index 0000000000..2c1cb02a42 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/TangemEmptyApplication.kt @@ -0,0 +1,11 @@ +package com.tangem.common + +import com.tangem.tap.TangemApplication + +open class TangemEmptyApplication : TangemApplication() { + + override fun onCreate() { + // super.onCreate() is not called intentionally + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/di/TangemMockedApplication.kt b/app/src/androidTest/kotlin/com/tangem/common/di/TangemMockedApplication.kt new file mode 100644 index 0000000000..06698555e9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/di/TangemMockedApplication.kt @@ -0,0 +1,7 @@ +package com.tangem.common.di + +import com.tangem.common.TangemEmptyApplication +import dagger.hilt.android.testing.CustomTestApplication + +@CustomTestApplication(TangemEmptyApplication::class) +internal class TangemMockedApplication \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/di/TestModule.kt b/app/src/androidTest/kotlin/com/tangem/common/di/TestModule.kt new file mode 100644 index 0000000000..106c04012d --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/di/TestModule.kt @@ -0,0 +1,28 @@ +package com.tangem.common.di + +import android.content.Context +import com.tangem.tap.di.TangemSdkManagerModule +import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager +import dagger.Module +import dagger.Provides +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.hilt.testing.TestInstallIn +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [TangemSdkManagerModule::class] +) +object TestModule { + + @Provides + @Singleton + fun provideTangemSdkManager( + @ApplicationContext context: Context + ): TangemSdkManager { + return MockTangemSdkManager(resources = context.resources) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt b/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt deleted file mode 100644 index 6e0bfc19b2..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/helpers/base/BaseAutoTestCase.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.helpers.base - -import android.Manifest -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.rule.GrantPermissionRule -import com.kaspersky.components.composesupport.config.withComposeSupport -import com.kaspersky.kaspresso.kaspresso.Kaspresso -import com.kaspersky.kaspresso.testcases.api.testcase.TestCase -import com.tangem.tap.MainActivity -import org.junit.Rule -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -open class BaseAutoTestCase : TestCase( - kaspressoBuilder = Kaspresso.Builder.withComposeSupport() -) { - - @get:Rule - open val composeTestRule = createAndroidComposeRule() - - @get: Rule - val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( - Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.CAMERA - ) -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt new file mode 100644 index 0000000000..51b94ab791 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerScreen.kt @@ -0,0 +1,17 @@ +package com.tangem.screens + +import com.kaspersky.kaspresso.screens.KScreen +import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment +import com.tangem.wallet.R +import io.github.kakaocup.kakao.text.KButton + +object DisclaimerScreen : KScreen(){ + + override val layoutId = R.layout.fragment_disclaimer + + override val viewClass = DisclaimerFragment::class.java + + val acceptButton: KButton = KButton { + withId(R.id.btn_accept) + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt index 38bdbce846..cda17d33c2 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/StoriesScreen.kt @@ -1,7 +1,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import com.tangem.tap.common.compose.resources.C +import com.tangem.core.ui.test.TestTags import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.KNode @@ -11,15 +11,15 @@ import io.github.kakaocup.kakao.text.KButton class StoriesScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen( semanticsProvider = semanticsProvider, - viewBuilderAction = { hasTestTag(C.Tag.STORIES_SCREEN) } + viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) } ) { val scanButton: KNode = child { - hasTestTag(C.Tag.STORIES_SCREEN_SCAN_BUTTON) + hasTestTag(TestTags.STORIES_SCREEN_SCAN_BUTTON) } val orderButton: KNode = child { - hasTestTag(C.Tag.STORIES_SCREEN_ORDER_BUTTON) + hasTestTag(TestTags.STORIES_SCREEN_ORDER_BUTTON) } val enableNFCAlert: KView = KView { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt new file mode 100644 index 0000000000..0988625537 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletScreen.kt @@ -0,0 +1,11 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.core.ui.test.TestTags +import io.github.kakaocup.compose.node.element.ComposeScreen + +class WalletScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TestTags.WALLET_SCREEN) } + ) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt new file mode 100644 index 0000000000..dcebde4556 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt @@ -0,0 +1,39 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.screens.DisclaimerScreen +import com.tangem.screens.StoriesScreen +import com.tangem.screens.WalletScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.compose.node.element.ComposeScreen +import org.junit.Test + +@HiltAndroidTest +class MainScreenTest : BaseTestCase() { + + @Test + fun goToMain() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button") { + scanButton { + assertIsDisplayed() + performClick() + } + } + } + DisclaimerScreen { + step("Click on \"Accept\" button") { + acceptButton { + isVisible() + click() + } + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Make sure wallet screen is visible") { + assertIsDisplayed() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt new file mode 100644 index 0000000000..a1d67d8c1c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanErrorTest.kt @@ -0,0 +1,48 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.screens.DisclaimerScreen +import com.tangem.screens.StoriesScreen +import com.tangem.screens.WalletScreen +import com.tangem.tap.domain.sdk.mocks.MockProvider +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.compose.node.element.ComposeScreen +import org.junit.Test + +@HiltAndroidTest +class ScanErrorTest : BaseTestCase() { + + @Test + fun goToMain() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button emulating scan error") { + MockProvider.setEmulateError() + scanButton { + assertIsDisplayed() + performClick() + } + } + step("Click on \"Scan\" button again without emulating error") { + MockProvider.resetEmulateError() + scanButton { + assertIsDisplayed() + performClick() + } + } + } + DisclaimerScreen { + step("Click on \"Accept\" button") { + acceptButton { + isVisible() + click() + } + } + } + ComposeScreen.onComposeScreen(composeTestRule) { + step("Make sure wallet screen is visible") { + assertIsDisplayed() + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index ce58b4a1be..d6418c9364 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -1,45 +1,43 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW -import androidx.test.espresso.intent.Intents -import com.tangem.helpers.base.BaseAutoTestCase +import com.tangem.common.BaseTestCase import com.tangem.screens.StoriesScreen import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL +import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.kakao.intent.KIntent import org.junit.Test -class StoriesTest : BaseAutoTestCase() { +@HiltAndroidTest +class StoriesTest : BaseTestCase() { @Test - fun clickOnButtons() = before { - Intents.init() - }.after { - Intents.release() - }.run { - ComposeScreen.onComposeScreen(composeTestRule) { - step("Click on \"Scan\" button") { - scanButton { - assertIsDisplayed() - performClick() + fun clickOnButtons() = + setupHooks().run { + ComposeScreen.onComposeScreen(composeTestRule) { + step("Click on \"Scan\" button") { + scanButton { + assertIsDisplayed() + performClick() + } } - } - step("Assert: \"Scan card\" popup opened") { - enableNFCAlert.isDisplayed() - cancelButton.click() - device.uiDevice.pressBack() - } - step("Click on \"Order\" button") { - orderButton.performClick() - } - step("Assert: browser opened") { - val expectedIntent = KIntent { - hasAction(ACTION_VIEW) - hasData(NEW_BUY_WALLET_URL) + step("Assert: \"Scan card\" popup opened") { + enableNFCAlert.isDisplayed() + cancelButton.click() + device.uiDevice.pressBack() + } + step("Click on \"Order\" button") { + orderButton.performClick() + } + step("Assert: browser opened") { + val expectedIntent = KIntent { + hasAction(ACTION_VIEW) + hasData(NEW_BUY_WALLET_URL) + } + expectedIntent.intended() + device.uiDevice.pressBack() } - expectedIntent.intended() - device.uiDevice.pressBack() } } - } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 093b3eb247..1d52735ce7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,6 @@ - @@ -36,7 +35,7 @@ @@ -170,11 +169,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() + val splashScreen = installSplashScreen() + installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation super.onCreate(savedInstanceState) + splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } + installActivityDependencies() observeAppThemeModeUpdates() @@ -217,7 +219,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(cardSdkConfigRepository.sdk, this) - lockUserWalletsTimer = LockUserWalletsTimer(owner = this) + lockUserWalletsTimer = LockUserWalletsTimer( + owner = this, + settingsRepository = settingsRepository, + userWalletsListManager = userWalletsListManager, + ) initIntentHandlers() @@ -232,6 +238,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac cardSdkConfigRepository = cardSdkConfigRepository, sendRouter = sendRouter, qrScanningRouter = qrScanningRouter, + emailSender = emailSender, ), ) } @@ -264,9 +271,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun createAppThemeModeFlow(): SharedFlow { - val tapApplication = application as TapApplication + val tangemApplication = application as TangemApplication - return tapApplication.getAppThemeModeUseCase() + return tangemApplication.getAppThemeModeUseCase() .map { maybeMode -> maybeMode.getOrElse { AppThemeMode.DEFAULT } } @@ -303,7 +310,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun initIntentHandlers() { - val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } + val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) } @@ -437,15 +444,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { - val canSaveWallets = if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { - runCatching { userWalletsListManager.asLockable()?.isLockedSync } - .fold(onSuccess = { true }, onFailure = { false }) - } else { - userWalletsListManager is BiometricUserWalletsListManager - } - val hasSavedWallets = userWalletsListManager.hasUserWallets - - if (canSaveWallets && hasSavedWallets) { + if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatch( NavigationAction.NavigateTo( screen = AppScreen.Welcome, diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt similarity index 69% rename from app/src/main/java/com/tangem/tap/TapApplication.kt rename to app/src/main/java/com/tangem/tap/TangemApplication.kt index 7c26fe7570..d731d06dd8 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 @@ -35,20 +32,22 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetFeedbackEmailUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler -import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.feedback.AdditionalFeedbackInfo @@ -61,134 +60,134 @@ 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 + // region DI + private val entryPoint: ApplicationEntryPoint + get() = EntryPoints.get(this, ApplicationEntryPoint::class.java) - @Inject - lateinit var configManager: ConfigManager + private val appStateHolder: AppStateHolder + get() = entryPoint.getAppStateHolder() - @Inject - lateinit var assetReader: AssetReader + private val configManager: ConfigManager + get() = entryPoint.getConfigManager() - @Inject - lateinit var featureTogglesManager: FeatureTogglesManager + private val 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 generateWalletNameUseCase: GenerateWalletNameUseCase + get() = entryPoint.getWalletNameGenerateUseCase() - @Inject - lateinit var wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase + private val cardRepository: CardRepository + get() = entryPoint.getCardRepository() - @Inject - lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles + get() = entryPoint.getFeedbackManagerFeatureToggles() - @Inject - lateinit var cardRepository: CardRepository + private val tangemSdkLogger: TangemSdkLogger + get() = entryPoint.getTangemSdkLogger() - @Inject - lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles + private val settingsRepository: SettingsRepository + get() = entryPoint.getSettingsRepository() - @Inject - lateinit var blockchainSDKLogger: BlockchainSDKLogger + private val blockchainSDKFactory: BlockchainSDKFactory + get() = entryPoint.getBlockchainSDKFactory() - @Inject - lateinit var tangemSdkLogger: TangemSdkLogger - // endregion Injected + private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase + get() = entryPoint.getGetFeedbackEmailUseCase() + + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase + get() = entryPoint.getSaveBlockchainErrorUseCase() + // endregion override fun onCreate() { super.onCreate() + init() + } + + fun init() { store = createReduxStore() if (BuildConfig.LOG_ENABLED) { @@ -206,19 +205,10 @@ internal class TapApplication : Application(), ImageLoaderFactory { activityResultCaller = foregroundActivityObserver registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) - preferencesStorage = preferencesDataSource - walletConnectRepository = WalletConnectRepository(this) - // TODO: Try to performance and user experience. // [REDACTED_JIRA] runBlocking { featureTogglesManager.init() - - if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { - store.dispatch(GlobalAction.UpdateUserWalletsListManager(generalUserWalletsListManager)) - } else { - initUserWalletsListManager() - } } val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) @@ -264,16 +254,16 @@ internal class TapApplication : Application(), ImageLoaderFactory { balanceHidingRepository = balanceHidingRepository, walletsRepository = walletsRepository, sendFeatureToggles = sendFeatureToggles, - blockchainDataStorage = blockchainDataStorage, - accountCreator = accountCreator, - userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles, generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, + generateWalletNameUseCase = generateWalletNameUseCase, cardRepository = cardRepository, feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, tangemSdkLogger = tangemSdkLogger, - blockchainSDKLogger = blockchainSDKLogger, + settingsRepository = settingsRepository, + blockchainSDKFactory = blockchainSDKFactory, + saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, ), ), ) @@ -305,7 +295,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { private fun initAnalytics(application: Application, config: Config) { val factory = AnalyticsFactory() factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder()) - factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addFilter(oneTimeEventFilter) @@ -366,6 +355,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, chatManager = ChatManager(foregroundActivityObserver), + feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, + getFeedbackEmailUseCase = getFeedbackEmailUseCase, ) store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager)) } @@ -373,14 +364,4 @@ internal class TapApplication : Application(), ImageLoaderFactory { private fun initWarningMessagesManager() { store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager())) } - - private suspend fun initUserWalletsListManager() { - val manager = if (walletsRepository.shouldSaveUserWalletsSync()) { - UserWalletsListManager.provideBiometricImplementation(applicationContext) - } else { - UserWalletsListManager.provideRuntimeImplementation() - } - - store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemHiltApplication.kt b/app/src/main/java/com/tangem/tap/TangemHiltApplication.kt new file mode 100644 index 0000000000..a818249392 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/TangemHiltApplication.kt @@ -0,0 +1,6 @@ +package com.tangem.tap + +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class TangemHiltApplication : TangemApplication() \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 5887deeadc..6b870386be 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -86,10 +86,6 @@ class DialogManager : StoreSubscriber { context = context, ) } - is WalletConnectDialog.ApproveWcSession -> - ApproveWcSessionDialog.create(state.dialog.session, state.dialog.networks, context) - is WalletConnectDialog.ChooseNetwork -> - ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context) is WalletConnectDialog.ClipboardOrScanQr -> ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context) is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 489effa20d..8cfba8dd64 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -13,20 +13,20 @@ sealed class AnalyticsParam { } sealed class CardBalanceState(val value: String) { - object Empty : CardBalanceState("Empty") - object Full : CardBalanceState("Full") + data object Empty : CardBalanceState("Empty") + data object Full : CardBalanceState("Full") companion object } sealed class RateApp(val value: String) { - object Liked : RateApp("Liked") - object Closed : RateApp("Close") + data object Liked : RateApp("Liked") + data object Closed : RateApp("Close") } sealed class OnOffState(val value: String) { - object On : OnOffState("On") - object Off : OnOffState("Off") + data object On : OnOffState("On") + data object Off : OnOffState("Off") companion object { @@ -35,13 +35,13 @@ sealed class AnalyticsParam { } sealed class UserCode(val value: String) { - object AccessCode : UserCode("Access Code") + data object AccessCode : UserCode("Access Code") } sealed class SecurityMode(val value: String) { - object AccessCode : SecurityMode("Access Code") - object Passcode : SecurityMode("Passcode") - object LongTap : SecurityMode("Long Tap") + data object AccessCode : SecurityMode("Access Code") + data object Passcode : SecurityMode("Passcode") + data object LongTap : SecurityMode("Long Tap") companion object { fun from(option: SecurityOption): SecurityMode = when (option) { @@ -56,8 +56,8 @@ sealed class AnalyticsParam { val key: String = "Status" - object Enabled : AccessCodeRecoveryStatus("Enabled") - object Disabled : AccessCodeRecoveryStatus("Disabled") + data object Enabled : AccessCodeRecoveryStatus("Enabled") + data object Disabled : AccessCodeRecoveryStatus("Disabled") companion object { fun from(enabled: Boolean): AccessCodeRecoveryStatus { @@ -67,21 +67,21 @@ sealed class AnalyticsParam { } sealed class Error(val value: String) { - object App : Error("App Error") - object CardSdk : Error("Card Sdk Error") - object BlockchainSdk : Error("Blockchain Sdk Error") + data object App : Error("App Error") + data object CardSdk : Error("Card Sdk Error") + data object BlockchainSdk : Error("Blockchain Sdk Error") } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType(value = "Private Key") - object NewSeed : WalletCreationType(value = "New Seed") - object SeedImport : WalletCreationType(value = "Seed Import") + data object PrivateKey : WalletCreationType(value = "Private Key") + data object NewSeed : WalletCreationType(value = "New Seed") + data object SeedImport : WalletCreationType(value = "Seed Import") } sealed class AppTheme(val value: String) { - object System : AppTheme("System") - object Dark : AppTheme("Dark") - object Light : AppTheme("Light") + data object System : AppTheme("System") + data object Dark : AppTheme("Dark") + data object Light : AppTheme("Light") companion object { fun fromAppThemeMode(mode: AppThemeMode): AppTheme { @@ -104,6 +104,7 @@ sealed class AnalyticsParam { const val PERMISSION_TYPE = "Permission Type" const val PRODUCT_TYPE = "Product Type" const val FIRMWARE = "Firmware" + const val USER_WALLET_ID = "User Wallet ID" const val CURRENCY = "Currency" const val ERROR_DESCRIPTION = "Error Description" const val ERROR_CODE = "Error Code" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt index 969d1d47fb..9bf97f2250 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.analytics.events +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.common.extensions.filterNotNull diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt deleted file mode 100644 index 710c7308e6..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.tap.common.analytics.handlers.appsFlyer - -import com.appsflyer.AFInAppEventType -import com.tangem.core.analytics.api.AnalyticsHandler -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder -import com.tangem.tap.common.analytics.events.Shop - -class AppsFlyerAnalyticsHandler( - private val client: AppsFlyerAnalyticsClient, -) : AnalyticsHandler { - - override fun id(): String = ID - - override fun send(eventId: String, params: Map) { - client.logEvent(eventId, params) - } - - override fun send(event: AnalyticsEvent) { - if (event is Shop.Purchased) { - send(AFInAppEventType.PURCHASE, event.params) - } else { - super.send(event) - } - } - - companion object { - const val ID = "AppsFlyer" - } - - class Builder : AnalyticsHandlerBuilder { - override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when { - !data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerDevKey) - data.isDebug && data.logConfig.appsFlyer -> AppsFlyerLogClient(data.jsonConverter) - else -> null - }?.let { AppsFlyerAnalyticsHandler(it) } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt deleted file mode 100644 index 40cee4e698..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerClient.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.tap.common.analytics.handlers.appsFlyer - -import android.content.Context -import com.appsflyer.AppsFlyerLib -import com.tangem.core.analytics.api.EventLogger - -/** -[REDACTED_AUTHOR] - */ -interface AppsFlyerAnalyticsClient : EventLogger - -internal class AppsFlyerClient( - private val context: Context, - key: String, -) : AppsFlyerAnalyticsClient { - - private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance() - - init { - appsFlyerLib.init(key, null, context) - appsFlyerLib.start(context) - } - - override fun logEvent(event: String, params: Map) { - appsFlyerLib.logEvent(context, event, params) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt deleted file mode 100644 index 8e87061b2c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.common.analytics.handlers.appsFlyer - -import com.tangem.common.json.MoshiJsonConverter -import com.tangem.tap.common.analytics.AnalyticsEventsLogger - -/** -[REDACTED_AUTHOR] - */ -internal class AppsFlyerLogClient( - jsonConverter: MoshiJsonConverter, -) : AppsFlyerAnalyticsClient { - - private val logger = AnalyticsEventsLogger(AppsFlyerAnalyticsHandler.ID, jsonConverter) - - override fun logEvent(event: String, params: Map) { - logger.logEvent(event, params) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index c881cd0127..e7c6feee04 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.IntroductionProcess @@ -18,6 +19,8 @@ class CardContextInterceptor( private val scanResponse: ScanResponse, ) : ParamsInterceptor { + private val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + override fun id(): String = CardContextInterceptor.id() override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { @@ -32,6 +35,9 @@ class CardContextInterceptor( params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse) params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue + if (userWalletId != null) { + params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue + } ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let { params[AnalyticsParam.CURRENCY] = it.value diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt new file mode 100644 index 0000000000..8b7ee07933 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.clipboard + +import android.content.ClipData +import com.tangem.core.ui.clipboard.ClipboardManager +import android.content.ClipboardManager as AndroidClipboardManager + +internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager { + + override fun setText(label: String, text: String) { + clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt new file mode 100644 index 0000000000..bdab75088f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.clipboard + +import com.tangem.core.ui.clipboard.ClipboardManager +import timber.log.Timber + +internal object MockClipboardManager : ClipboardManager { + override fun setText(label: String, text: String) { + /** Intentionnaly do nothing */ + Timber.w("Clipboard Manager not available") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt b/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt deleted file mode 100644 index ca77ba62d2..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/resources/C.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.common.compose.resources - -object C { - object Tag { - const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER" - const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" - const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index f4df731c74..5347ee2efc 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchainsdk.utils.amountToCreateAccount import com.tangem.common.services.Result -import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.TestActions import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 975fb1fb2f..e87ca04616 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -6,7 +6,7 @@ import com.tangem.blockchain.common.address.Address import com.tangem.crypto.NetworkType import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.extensions.stripZeroPlainString import java.util.concurrent.CopyOnWriteArrayList diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index 83a14893d8..98aa013e84 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -1,13 +1,22 @@ package com.tangem.tap.common.feedback import android.content.Context +import com.tangem.core.navigation.email.EmailSender import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.common.TapWorkarounds +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.tap.common.chat.ChatManager +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.mainScope +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import com.tangem.tap.withForegroundActivity +import kotlinx.coroutines.launch import timber.log.Timber import java.io.File import java.io.FileWriter @@ -20,21 +29,46 @@ class FeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, private val chatManager: ChatManager, + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, + private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase, ) { private var sessionFeedbackFile: File? = null private var sessionLogsFile: File? = null fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { - feedbackData.prepare(infoHolder) - foregroundActivityObserver.withForegroundActivity { activity -> - activity.sendEmail( - email = getSupportEmail(), - subject = activity.getString(feedbackData.subjectResId), - message = feedbackData.joinTogether(activity, infoHolder), - file = getLogFile(activity), - onFail = onFail, - ) + if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { + mainScope.launch { + val email = getFeedbackEmailUseCase( + when (feedbackData) { + is FeedbackEmail -> FeedbackEmailType.DirectUserRequest + is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter + is ScanFailsEmail -> FeedbackEmailType.ScanningProblem + is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem + else -> FeedbackEmailType.DirectUserRequest + }, + ) + + store.inject(DaggerGraphState::emailSender).send( + email = EmailSender.Email( + address = email.address, + subject = email.subject, + message = email.message, + attachment = email.file, + ), + ) + } + } else { + feedbackData.prepare(infoHolder) + foregroundActivityObserver.withForegroundActivity { activity -> + activity.sendEmail( + email = getSupportEmail(), + subject = activity.getString(feedbackData.subjectResId), + message = feedbackData.joinTogether(activity, infoHolder), + file = getLogFile(activity), + onFail = onFail, + ) + } } } diff --git a/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt b/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt new file mode 100644 index 0000000000..688dea7f67 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/haptic/DefaultHapticManager.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.common.haptic + +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import com.tangem.core.ui.haptic.HapticManager + +class DefaultHapticManager(private val vibrator: Vibrator) : HapticManager { + + override fun vibrateShort() { + vibrate(VIBRATION_SHORT_DURATION) + } + + override fun vibrateMeduim() { + vibrate(VIBRATION_MEDIUM_LOW_DURATION) + } + + override fun vibrateLong() { + vibrate(VIBRATION_MEDIUM_DURATION) + } + + private fun vibrate(durationMs: Long) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + vibrator.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + vibrator.vibrate(durationMs) + } + } + + companion object { + const val VIBRATION_SHORT_DURATION = 50L + const val VIBRATION_MEDIUM_LOW_DURATION = 75L + const val VIBRATION_MEDIUM_DURATION = 100L + const val VIBRATION_LONG_DURATION = 200L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt index c8fa6db166..57be8c6e21 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt @@ -3,9 +3,10 @@ package com.tangem.tap.common.redux import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.preferencesStorage +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store +import kotlinx.coroutines.launch import org.rekotlin.Middleware class AccessCodeRequestPolicyMiddleware { @@ -21,8 +22,12 @@ class AccessCodeRequestPolicyMiddleware { } private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) { - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet, - ) + mainScope.launch { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet, + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a9c23d448f..8439279a0a 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.redux.global -import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.navigation.StateDialog @@ -9,7 +8,6 @@ import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.feedback.FeedbackData import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.common.redux.DebugErrorAction @@ -85,7 +83,6 @@ sealed class GlobalAction : Action { data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() - data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() object ExchangeManager : GlobalAction() { object Init : GlobalAction() { @@ -101,6 +98,5 @@ sealed class GlobalAction : Action { data class Success(val countryCode: String) : GlobalAction() } - data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction() data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 921898e383..fc584f6b8e 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -3,9 +3,7 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard -import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.models.Config import com.tangem.domain.appcurrency.model.AppCurrency @@ -15,7 +13,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.send.redux.SendAction -import com.tangem.tap.mainScope import com.tangem.tap.network.exchangeServices.BuyExchangeService import com.tangem.tap.network.exchangeServices.CardExchangeRules import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -26,13 +23,10 @@ import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware -import timber.log.Timber import java.util.Locale object GlobalMiddleware { @@ -97,16 +91,13 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } feedbackManager.openChat(chatConfig, action.feedbackData) } - is GlobalAction.UpdateFeedbackInfo -> { - store.state.globalState.feedbackManager?.infoHolder - ?.setWalletsInfo(action.walletManagers) - } is GlobalAction.ExchangeManager.Init -> { val appStateSafe = appState() ?: return val config = appStateSafe.globalState.configManager?.config ?: return scope.launch { val scanResponseProvider: () -> ScanResponse? = { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.selectedUserWalletSync?.scanResponse } val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card } @@ -148,33 +139,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } } } - is GlobalAction.UpdateUserWalletsListManager -> { - val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade) - - /* - * If implementation of the UserWalletsListManager is changed, - * then all observers of selectedUserWallet become irrelevant. - */ - action.manager.selectedUserWallet - .distinctUntilChanged() - .onEach { userWallet -> - Analytics.setContext(userWallet.scanResponse) - Analytics.send(Basic.WalletOpened()) - - store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder -> - infoHolder.setCardInfo(userWallet.scanResponse) - - walletManagersFacade - .getAll(userWallet.walletId) - .distinctUntilChanged() - .onEach(infoHolder::setWalletsInfo) - .catch { Timber.e(it) } - .launchIn(mainScope) - } - } - .flowOn(Dispatchers.IO) - .launchIn(scope) - } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index fbe3e38b89..d85d35f457 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -3,12 +3,10 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.proxy.AppStateHolder -import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.extensions.replaceBy import org.rekotlin.Action @@ -94,19 +92,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde userCountryCode = action.countryCode, ) } - is GlobalAction.UpdateUserWalletsListManager -> { - val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - - if (featureToggles.isGeneralManagerEnabled) { - val generalUserWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - appStateHolder.userWalletsListManager = generalUserWalletsListManager - globalState.copy(userWalletsListManager = generalUserWalletsListManager) - } else { - appStateHolder.userWalletsListManager = action.manager - globalState.copy(userWalletsListManager = action.manager) - } - } is GlobalAction.ChangeAppThemeMode -> globalState.copy( appThemeMode = action.appThemeMode, ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 20712881c8..7a75d16d87 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.feedback.FeedbackManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager @@ -27,7 +26,6 @@ data class GlobalState( val dialog: StateDialog? = null, val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val userCountryCode: String? = null, - val userWalletsListManager: UserWalletsListManager? = null, val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 87b5944231..bd50b28faf 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -1,7 +1,7 @@ package com.tangem.tap.data import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.firstOrNull @@ -9,26 +9,24 @@ import kotlinx.coroutines.flow.firstOrNull // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented // [REDACTED_JIRA] internal class RuntimeUserWalletsStore( - private val walletsStateHolder: WalletsStateHolder, + private val userWalletsListManager: UserWalletsListManager, ) : UserWalletsStore { override val selectedUserWalletOrNull: UserWallet? - get() = walletsStateHolder.userWalletsListManager?.selectedUserWalletSync + get() = userWalletsListManager.selectedUserWalletSync override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { - return walletsStateHolder.userWalletsListManager - ?.userWallets - ?.firstOrNull() + return userWalletsListManager + .userWallets + .firstOrNull() ?.singleOrNull { it.walletId == key } } override suspend fun getAllSyncOrNull(): List? { - return walletsStateHolder.userWalletsListManager - ?.userWallets - ?.firstOrNull() + return userWalletsListManager.userWallets.firstOrNull() } override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) { - walletsStateHolder.userWalletsListManager?.update(userWalletId, update) + userWalletsListManager.update(userWalletId, update) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 9a0f838f4a..cbf8eebb27 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,20 +1,20 @@ package com.tangem.tap.di -import android.content.Context import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.feature.tester.ActivityClassWrapper +import com.tangem.tap.MainActivity +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.proxy.AppStateHolder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -25,15 +25,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object ActivityModule { - @Provides - @Singleton - fun provideTangemSdkManager( - @ApplicationContext context: Context, - cardSdkConfigRepository: CardSdkConfigRepository, - ): TangemSdkManager { - return TangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources) - } - @Provides @Singleton fun provideScanCardUseCase( @@ -76,4 +67,10 @@ internal object ActivityModule { ): GetPolkadotCheckHasImmortalUseCase { return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository) } + + @Provides + @Singleton + fun provideActivityClassWrapper(): ActivityClassWrapper { + return ActivityClassWrapper(MainActivity::class.java) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt index e1bf363dd7..c6e0267c6b 100644 --- a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt +++ b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt @@ -2,7 +2,6 @@ package com.tangem.tap.di import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.tap.proxy.AppStateHolder import dagger.Binds import dagger.Module @@ -14,10 +13,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface AppStateHolderModule { - @Binds - @Singleton - fun bindsWalletsStateHolder(appStateHolder: AppStateHolder): WalletsStateHolder - @Binds @Singleton fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController diff --git a/app/src/main/java/com/tangem/tap/di/ClipboardManagerModule.kt b/app/src/main/java/com/tangem/tap/di/ClipboardManagerModule.kt new file mode 100644 index 0000000000..052f1c4541 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/ClipboardManagerModule.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.di + +import android.content.Context +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.tap.common.clipboard.MockClipboardManager +import com.tangem.tap.common.clipboard.DefaultClipboardManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import android.content.ClipboardManager as AndroidClipboardManager + +@Module +@InstallIn(SingletonComponent::class) +internal class ClipboardManagerModule { + + @Provides + @Singleton + fun provideClipboardManager(@ApplicationContext context: Context): ClipboardManager { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? AndroidClipboardManager + + return if (clipboardManager != null) { + DefaultClipboardManager(clipboardManager = clipboardManager) + } else { + MockClipboardManager + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/HapticModule.kt b/app/src/main/java/com/tangem/tap/di/HapticModule.kt new file mode 100644 index 0000000000..bf8439f09e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/HapticModule.kt @@ -0,0 +1,37 @@ +package com.tangem.tap.di + +import android.content.Context +import android.os.Build +import android.os.Vibrator +import android.os.VibratorManager +import com.tangem.tap.common.haptic.DefaultHapticManager +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.MockHapticManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class HapticModule { + + @Provides + @Singleton + fun provideHapticManager(@ApplicationContext context: Context): HapticManager { + val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vibratorManager.defaultVibrator + } else { + context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + + return if (vibrator.hasVibrator()) { + DefaultHapticManager(vibrator = vibrator) + } else { + MockHapticManager + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt new file mode 100644 index 0000000000..c6118d1ac1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.di + +import android.content.Context +import com.tangem.domain.card.BuildConfig +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager +import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class TangemSdkManagerModule { + + @Provides + @Singleton + fun provideTangemSdkManager( + @ApplicationContext context: Context, + cardSdkConfigRepository: CardSdkConfigRepository, + ): TangemSdkManager { + return if (BuildConfig.MOCK_DATA_SOURCE) { + MockTangemSdkManager(resources = context.resources) + } else { + DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/ThemeModule.kt b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt index af2732b2d9..e4b2deeb8d 100644 --- a/app/src/main/java/com/tangem/tap/di/ThemeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ThemeModule.kt @@ -5,13 +5,15 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ActivityComponent::class) +@InstallIn(SingletonComponent::class) internal object ThemeModule { @Provides + @Singleton fun provideAppThemeModeHolder(): AppThemeModeHolder { return MutableAppThemeModeHolder } diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt new file mode 100644 index 0000000000..97b0f48bee --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di + +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.theme.AppThemeModeHolder +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object UiDependenciesModule { + + @Provides + @Singleton + fun provideUiDependencies(hapticManager: HapticManager, appThemeModeHolder: AppThemeModeHolder): UiDependencies { + return object : UiDependencies { + override val hapticManager = hapticManager + override val appThemeModeHolder = appThemeModeHolder + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt index 0f7457c20f..b87e417fab 100644 --- a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt @@ -2,7 +2,7 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.card.DefaultDerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt index 6aeaad1dcf..b2a04f9d52 100644 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.data.RuntimeUserWalletsStore import dagger.Module import dagger.Provides @@ -15,7 +15,7 @@ internal object UserWalletsStoreModule { @Provides @Singleton - fun provideUserWalletsStore(walletsStateHolder: WalletsStateHolder): UserWalletsStore { - return RuntimeUserWalletsStore(walletsStateHolder) + fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore { + return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 7a05bef47d..f49789df71 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -6,10 +6,10 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase -import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase +import com.tangem.tap.domain.sdk.TangemSdkManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -36,14 +36,6 @@ internal object CardDomainModule { return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository) } - @Provides - @ViewModelScoped - fun provideGetAccessCodeSavingStatusUseCase( - cardSdkConfigRepository: CardSdkConfigRepository, - ): GetAccessCodeSavingStatusUseCase { - return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) - } - @Provides @ViewModelScoped fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase { @@ -68,8 +60,8 @@ internal object CardDomainModule { @Provides @ViewModelScoped - fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase { - return IsNeedToBackupUseCase(walletStateHolder) + fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index 3cfbe41b23..9654238c85 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -1,6 +1,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.* +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import dagger.Module import dagger.Provides @@ -14,5 +16,11 @@ internal object CardLegacyDomainModule { @Provides @Singleton - fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() + fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor() + + @Provides + @Singleton + fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase { + return GenerateWalletNameUseCase(userWalletsListManager) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt index f2aea82f57..033e322536 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -1,7 +1,8 @@ package com.tangem.tap.di.domain import android.content.Context -import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase +import com.tangem.domain.feedback.GetFeedbackEmailUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.repository.FeedbackRepository import dagger.Module import dagger.Provides @@ -16,10 +17,16 @@ internal object FeedbackDomainModule { @Provides @Singleton - fun provideGetFeedbackToSupportUseCase( + fun provideGetFeedbackEmailUseCase( feedbackRepository: FeedbackRepository, @ApplicationContext context: Context, - ): GetSupportFeedbackEmailUseCase { - return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources) + ): GetFeedbackEmailUseCase { + return GetFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources) + } + + @Provides + @Singleton + fun provideSaveBlockchainErrorUseCase(feedbackRepository: FeedbackRepository): SaveBlockchainErrorUseCase { + return SaveBlockchainErrorUseCase(feedbackRepository = feedbackRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 629af1e441..22502ca5c5 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.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import dagger.Module import dagger.Provides @@ -148,4 +148,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 662e68f7bb..0067fa4c3a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -6,7 +6,6 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -55,9 +54,8 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetTokenListUseCase { - return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository) } @Provides @@ -66,9 +64,8 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetCardTokensListUseCase { - return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository) } @Provides @@ -188,20 +185,20 @@ internal object TokensDomainModule { @ViewModelScoped fun provideGetCryptoCurrencyActionsUseCase( rampStateManager: RampStateManager, + walletManagersFacade: WalletManagersFacade, marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - sendFeatureToggles: SendFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( rampManager = rampStateManager, + walletManagersFacade = walletManagersFacade, marketCryptoCurrencyRepository = marketCryptoCurrencyRepository, currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, - sendFeatureToggles = sendFeatureToggles, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index bf4f00059e..bb6a62fc78 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -2,12 +2,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides @@ -43,6 +42,22 @@ internal object TransactionDomainModule { ) } + @Provides + @ViewModelScoped + fun provideAssociateAssetUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + walletManagersFacade: WalletManagersFacade, + currenciesRepository: CurrenciesRepository, + networksRepository: NetworksRepository, + ): AssociateAssetUseCase { + return AssociateAssetUseCase( + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + ) + } + @Provides @ViewModelScoped fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { @@ -54,4 +69,10 @@ internal object TransactionDomainModule { fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { return IsFeeApproximateUseCase(feeRepository) } + + @Provides + @ViewModelScoped + fun provideValidateTransactionUseCase(transactionRepository: TransactionRepository): ValidateTransactionUseCase { + return ValidateTransactionUseCase(transactionRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt index 319ddd2c0e..2bd8c1d4c1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -1,18 +1,13 @@ package com.tangem.tap.di.domain import com.squareup.moshi.Moshi -import com.tangem.blockchain.common.AccountCreator -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.blockchain.common.logging.BlockchainSDKLogger -import com.tangem.datasource.asset.AssetReader -import com.tangem.datasource.config.ConfigManager +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore -import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.walletmanager.DefaultWalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.feature.onboarding.data.MnemonicRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -28,26 +23,16 @@ internal object WalletManagersFacadeModule { fun provideWalletManagersFacade( walletManagersStore: WalletManagersStore, userWalletsStore: UserWalletsStore, - configManager: ConfigManager, - blockchainDataStorage: BlockchainDataStorage, - accountCreator: AccountCreator, - mnemonicRepository: MnemonicRepository, assetReader: AssetReader, @SdkMoshi moshi: Moshi, - blockchainSDKLogger: BlockchainSDKLogger, - feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, + blockchainSDKFactory: BlockchainSDKFactory, ): WalletManagersFacade { return DefaultWalletManagersFacade( walletManagersStore = walletManagersStore, userWalletsStore = userWalletsStore, - configManager = configManager, - blockchainDataStorage = blockchainDataStorage, assetReader = assetReader, moshi = moshi, - mnemonic = mnemonicRepository.generateDefaultMnemonic(), - accountCreator = accountCreator, - blockchainSDKLogger = blockchainSDKLogger, - feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, + blockchainSDKFactory = blockchainSDKFactory, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index c7f58a8226..444ce645e5 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -2,10 +2,12 @@ package com.tangem.tap.di.domain import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletAddressServiceRepository +import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* +import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -19,32 +21,46 @@ internal object WalletsDomainModule { @Provides @ViewModelScoped - fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase { - return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { + return GetWalletsUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesGetUserWalletUseCase(walletsStateHolder: WalletsStateHolder): GetUserWalletUseCase { - return GetUserWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesWalletNameMigrationUseCase( + userWalletsListManager: UserWalletsListManager, + walletNamesMigrationRepository: WalletNamesMigrationRepository, + ): WalletNameMigrationUseCase { + return WalletNameMigrationUseCase( + userWalletsListManager = userWalletsListManager, + walletNamesMigrationRepository = walletNamesMigrationRepository, + ) } @Provides @ViewModelScoped - fun providesGetSelectedWalletSyncUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase { + return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase { - return GetSelectedWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetSelectedWalletSyncUseCase( + userWalletsListManager: UserWalletsListManager, + ): GetSelectedWalletSyncUseCase { + return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { - return SaveWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase { + return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager) + } + + @Provides + @ViewModelScoped + fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase { + return SaveWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides @@ -55,29 +71,41 @@ internal object WalletsDomainModule { @Provides @ViewModelScoped - fun providesUnlockWalletUseCase(walletsStateHolder: WalletsStateHolder): UnlockWalletsUseCase { - return UnlockWalletsUseCase(walletsStateHolder = walletsStateHolder) + fun providesUnlockWalletUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase { + return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped fun providesSelectWalletUseCase( - walletsStateHolder: WalletsStateHolder, + userWalletsListManager: UserWalletsListManager, reduxStateHolder: ReduxStateHolder, ): SelectWalletUseCase { - return SelectWalletUseCase(walletsStateHolder, reduxStateHolder) + return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder) } @Provides @ViewModelScoped - fun providesUpdateWalletUseCase(walletsStateHolder: WalletsStateHolder): UpdateWalletUseCase { - return UpdateWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase { + return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides @ViewModelScoped - fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase { - return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder) + fun providesRenameWalletUseCase(userWalletsListManager: UserWalletsListManager): RenameWalletUseCase { + return RenameWalletUseCase(userWalletsListManager = userWalletsListManager) + } + + @Provides + @ViewModelScoped + fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase { + return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager) + } + + @Provides + @ViewModelScoped + fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase { + return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager) } @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index f8a93381e8..256c8f1bf9 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,9 +1,7 @@ package com.tangem.tap.domain -import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic import com.tangem.datasource.config.ConfigManager @@ -15,7 +13,6 @@ import com.tangem.operations.attestation.Attestation import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction @@ -32,29 +29,12 @@ class TapWalletManager( private val dispatchers: CoroutineDispatcherProvider = AppCoroutineDispatcherProvider(), ) { - private val blockchainSdkConfig by lazy { - store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() - } - private var loadUserWalletDataJob: Job? = null set(value) { field?.cancel() field = value } - val walletManagerFactory: WalletManagerFactory by lazy { - WalletManagerFactory( - config = blockchainSdkConfig, - accountCreator = store.inject(DaggerGraphState::accountCreator), - blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage), - loggers = if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { - listOf(store.inject(DaggerGraphState::blockchainSDKLogger)) - } else { - emptyList() - }, - ) - } - suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) { // If a previous job was running, it gets cancelled before the new one starts, // ensuring that only one job is active at any given time. @@ -73,15 +53,18 @@ class TapWalletManager( val attestationFailed = card.attestation.status == Attestation.Status.Failed tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) - store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse) + + val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) + if (!featureToggles.isLocalLogsEnabled) { + store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse) + } + updateConfigManager(scanResponse) withMainContext { // Order is important store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer())) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - store.dispatch(WalletConnectAction.ResetState) store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) - store.dispatch(WalletConnectAction.RestoreSessions(scanResponse)) store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) } } diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt index b0fadb5bcf..8834604935 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.card.DeleteSavedAccessCodesUseCase -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager internal class DefaultDeleteSavedAccessCodesUseCase( private val tangemSdkManager: TangemSdkManager, diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 84d55370ed..b7ad02d630 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -9,11 +9,11 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import timber.log.Timber diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index c71548c0bd..9497f54791 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -5,7 +5,7 @@ import com.tangem.common.services.Result import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.userwallets.Artwork +import com.tangem.domain.wallets.models.Artwork import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier @@ -20,8 +20,8 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result Artwork.MARTA_CARD_URL else -> { when (getTwinCardNumber()) { - TwinCardNumber.First -> Artwork.TWIN_CARD_1 - TwinCardNumber.Second -> Artwork.TWIN_CARD_2 + TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL + TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL else -> Artwork.DEFAULT_IMG_URL } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index 8aeacbb2c9..ee417278e9 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard.repository import com.tangem.common.CompletionResult import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter // TODO: Move to the :data:card module diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt new file mode 100644 index 0000000000..7a576f77b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt @@ -0,0 +1,128 @@ +package com.tangem.tap.domain.sdk + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.tangem.Message +import com.tangem.common.* +import com.tangem.common.authentication.keystore.KeystoreManager +import com.tangem.common.core.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.secure.SecureStorage +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +@Suppress("TooManyFunctions") +interface TangemSdkManager { + + val canUseBiometry: Boolean + + val needEnrollBiometrics: Boolean + + val keystoreManager: KeystoreManager + + val secureStorage: SecureStorage + + val userCodeRequestPolicy: UserCodeRequestPolicy + + suspend fun scanProduct( + cardId: String? = null, + messageRes: Int? = null, + allowsRequestAccessCodeFromRepository: Boolean = false, + ): CompletionResult + + suspend fun createProductWallet( + scanResponse: ScanResponse, + shouldReset: Boolean = false, + ): CompletionResult + + // Wallet2 specific + suspend fun importWallet( + scanResponse: ScanResponse, + mnemonic: String, + passphrase: String?, + shouldReset: Boolean, + ): CompletionResult + + suspend fun derivePublicKeys( + cardId: String?, + derivations: Map>, + ): CompletionResult + + suspend fun deriveExtendedPublicKey( + cardId: String?, + walletPublicKey: ByteArray, + derivation: DerivationPath, + ): CompletionResult + + suspend fun resetToFactorySettings( + cardId: String, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult + + suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult + + suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult + + suspend fun clearSavedUserCodes(): CompletionResult + + suspend fun setPasscode(cardId: String?): CompletionResult + + suspend fun setAccessCode(cardId: String?): CompletionResult + + suspend fun setLongTap(cardId: String?): CompletionResult + + suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult + + suspend fun scanCard( + cardId: String? = null, + allowRequestAccessCodeFromRepository: Boolean = false, + ): CompletionResult + + @Deprecated( + "TangemSdkManager shouldn't run custom tasks. " + + "All of them should be specified in TangemSdkManager certain methods.", + ) + suspend fun runTaskAsync( + runnable: CardSessionRunnable, + cardId: String? = null, + initialMessage: Message? = null, + accessCode: String? = null, + @DrawableRes iconScanRes: Int? = null, + ): CompletionResult + + @Suppress("MagicNumber") + fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) + + @Deprecated("TangemSdkManager shouldn't returns a string from resources") + fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String + + fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) + + // region Twin-specific + + suspend fun finalizeTwin( + secondCardPublicKey: ByteArray, + issuerKeyPair: KeyPair, + cardId: String, + initialMessage: Message, + ): CompletionResult + + suspend fun createFirstTwinWallet(cardId: String, initialMessage: Message): CompletionResult + + @Suppress("LongParameterList") + suspend fun createSecondTwinWallet( + firstPublicKey: String, + firstCardId: String, + issuerKeys: KeyPair, + preparingMessage: Message, + creatingWalletMessage: Message, + initialMessage: Message, + ): CompletionResult + + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt similarity index 71% rename from app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt rename to app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 4097061289..3955158fd9 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain +package com.tangem.tap.domain.sdk.impl import android.content.res.Resources import androidx.annotation.DrawableRes @@ -28,11 +28,16 @@ import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.operations.derivation.DeriveWalletPublicKeyTask +import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.tap.derivationsFinder +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask +import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask +import com.tangem.tap.domain.twins.FinalizeTwinTask import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine @@ -40,10 +45,10 @@ import kotlinx.coroutines.withContext import kotlin.coroutines.resume @Suppress("TooManyFunctions") -class TangemSdkManager( +class DefaultTangemSdkManager( private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources, -) { +) : TangemSdkManager { private val tangemSdk: TangemSdk get() = cardSdkConfigRepository.sdk @@ -55,25 +60,25 @@ class TangemSdkManager( ) } - val canUseBiometry: Boolean + override val canUseBiometry: Boolean get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics - val needEnrollBiometrics: Boolean + override val needEnrollBiometrics: Boolean get() = tangemSdk.authenticationManager.needEnrollBiometrics - val keystoreManager: KeystoreManager + override val keystoreManager: KeystoreManager get() = tangemSdk.keystoreManager - val secureStorage: SecureStorage + override val secureStorage: SecureStorage get() = tangemSdk.secureStorage - val userCodeRequestPolicy: UserCodeRequestPolicy + override val userCodeRequestPolicy: UserCodeRequestPolicy get() = tangemSdk.config.userCodeRequestPolicy - suspend fun scanProduct( - cardId: String? = null, - messageRes: Int? = null, - allowsRequestAccessCodeFromRepository: Boolean = false, + override suspend fun scanProduct( + cardId: String?, + messageRes: Int?, + allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( @@ -87,9 +92,9 @@ class TangemSdkManager( ).also { sendScanResultsToAnalytics(it) } } - suspend fun createProductWallet( + override suspend fun createProductWallet( scanResponse: ScanResponse, - shouldReset: Boolean = false, + shouldReset: Boolean, ): CompletionResult { return runTaskAsync( runnable = CreateProductWalletTask( @@ -103,7 +108,7 @@ class TangemSdkManager( ) } - suspend fun importWallet( + override suspend fun importWallet( scanResponse: ScanResponse, mnemonic: String, passphrase: String?, @@ -135,14 +140,14 @@ class TangemSdkManager( } } - suspend fun derivePublicKeys( + override suspend fun derivePublicKeys( cardId: String?, derivations: Map>, ): CompletionResult { return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } - suspend fun deriveExtendedPublicKey( + override suspend fun deriveExtendedPublicKey( cardId: String?, walletPublicKey: ByteArray, derivation: DerivationPath, @@ -153,7 +158,7 @@ class TangemSdkManager( ) } - suspend fun resetToFactorySettings( + override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { @@ -167,7 +172,7 @@ class TangemSdkManager( .map { CardDTO(it) } } - suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { + override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { return userCodeRepository.save( cardsIds = cardsIds, userCode = UserCode( @@ -177,15 +182,15 @@ class TangemSdkManager( ) } - suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { + override suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { return userCodeRepository.delete(cardsIds.toSet()) } - suspend fun clearSavedUserCodes(): CompletionResult { + override suspend fun clearSavedUserCodes(): CompletionResult { return userCodeRepository.clear() } - suspend fun setPasscode(cardId: String?): CompletionResult { + override suspend fun setPasscode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), cardId, @@ -193,7 +198,7 @@ class TangemSdkManager( ) } - suspend fun setAccessCode(cardId: String?): CompletionResult { + override suspend fun setAccessCode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, @@ -201,7 +206,7 @@ class TangemSdkManager( ) } - suspend fun setLongTap(cardId: String?): CompletionResult { + override suspend fun setLongTap(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, @@ -209,7 +214,10 @@ class TangemSdkManager( ) } - suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult { + override suspend fun setAccessCodeRecoveryEnabled( + cardId: String?, + enabled: Boolean, + ): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeRecoveryAllowedTask(enabled), cardId, @@ -217,9 +225,9 @@ class TangemSdkManager( ) } - suspend fun scanCard( - cardId: String? = null, - allowRequestAccessCodeFromRepository: Boolean = false, + override suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromRepository: Boolean, ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = ScanTask(allowRequestAccessCodeFromRepository), @@ -229,12 +237,12 @@ class TangemSdkManager( .map { CardDTO(it) } } - suspend fun runTaskAsync( + override suspend fun runTaskAsync( runnable: CardSessionRunnable, - cardId: String? = null, - initialMessage: Message? = null, - accessCode: String? = null, - @DrawableRes iconScanRes: Int? = null, + cardId: String?, + initialMessage: Message?, + accessCode: String?, + @DrawableRes iconScanRes: Int?, ): CompletionResult = withContext(Dispatchers.Main) { suspendCancellableCoroutine { continuation -> tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode, iconScanRes) { result -> @@ -253,7 +261,7 @@ class TangemSdkManager( } @Suppress("MagicNumber") - fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) { + override fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) { tangemSdk.config.cardIdDisplayFormat = when { scanResponse == null -> CardIdDisplayFormat.Full scanResponse.cardTypesResolver.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4) @@ -262,14 +270,60 @@ class TangemSdkManager( } @Deprecated("TangemSdkManager shouldn't returns a string from resources") - fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { + override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { return resources.getString(stringResId, *formatArgs) } - fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { + override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { tangemSdk.config.userCodeRequestPolicy = policy } + // region Twin-specific + + override suspend fun createFirstTwinWallet( + cardId: String, + initialMessage: Message, + ): CompletionResult { + return runTaskAsync( + runnable = CreateFirstTwinWalletTask(cardId), + cardId = cardId, + initialMessage = initialMessage, + ) + } + + override suspend fun createSecondTwinWallet( + firstPublicKey: String, + firstCardId: String, + issuerKeys: KeyPair, + preparingMessage: Message, + creatingWalletMessage: Message, + initialMessage: Message, + ): CompletionResult { + val task = CreateSecondTwinWalletTask( + firstPublicKey = firstPublicKey, + firstCardId = firstCardId, + issuerKeys = issuerKeys, + preparingMessage = preparingMessage, + creatingWalletMessage = creatingWalletMessage, + ) + return runTaskAsync(task, null, initialMessage) + } + + override suspend fun finalizeTwin( + secondCardPublicKey: ByteArray, + issuerKeyPair: KeyPair, + cardId: String, + initialMessage: Message, + ): CompletionResult { + return runTaskAsync( + runnable = FinalizeTwinTask(secondCardPublicKey, issuerKeyPair), + cardId = cardId, + initialMessage = initialMessage, + ) + } + + // endregion + companion object { @Deprecated("Use [DefaultCardSdkProvider] instead") val config = Config( diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt new file mode 100644 index 0000000000..e690748bd5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -0,0 +1,175 @@ +package com.tangem.tap.domain.sdk.impl + +import android.content.res.Resources +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.tangem.Message +import com.tangem.common.* +import com.tangem.common.authentication.keystore.DummyKeystoreManager +import com.tangem.common.core.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.InMemoryStorage +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.domain.sdk.mocks.MockProvider +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +@Suppress("TooManyFunctions") +class MockTangemSdkManager( + private val resources: Resources, +) : TangemSdkManager { + + override val canUseBiometry = false + + override val needEnrollBiometrics = false + + override val keystoreManager = DummyKeystoreManager() + + override val secureStorage = InMemoryStorage() + + override val userCodeRequestPolicy: UserCodeRequestPolicy + get() = userCodeRequestPolicyInternal + + private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default + + override suspend fun scanProduct( + cardId: String?, + messageRes: Int?, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + return MockProvider.getScanResponse() + } + + override suspend fun createProductWallet( + scanResponse: ScanResponse, + shouldReset: Boolean, + ): CompletionResult { + return MockProvider.getCreateProductWalletResponse() + } + + override suspend fun importWallet( + scanResponse: ScanResponse, + mnemonic: String, + passphrase: String?, + shouldReset: Boolean, + ): CompletionResult { + return MockProvider.getImportWalletResponse() + } + + override suspend fun derivePublicKeys( + cardId: String?, + derivations: Map>, + ): CompletionResult { + return MockProvider.getDerivationTaskResponse() + } + + override suspend fun deriveExtendedPublicKey( + cardId: String?, + walletPublicKey: ByteArray, + derivation: DerivationPath, + ): CompletionResult { + return MockProvider.getExtendedPublicKey() + } + + override suspend fun resetToFactorySettings( + cardId: String, + allowsRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + return MockProvider.getCardDto() + } + + override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun deleteSavedUserCodes(cardsIds: Set): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun clearSavedUserCodes(): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun setPasscode(cardId: String?): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun setAccessCode(cardId: String?): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun setLongTap(cardId: String?): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun setAccessCodeRecoveryEnabled( + cardId: String?, + enabled: Boolean, + ): CompletionResult { + return MockProvider.getSuccessResponse() + } + + override suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromRepository: Boolean, + ): CompletionResult { + return MockProvider.getCardDto() + } + + override suspend fun runTaskAsync( + runnable: CardSessionRunnable, + cardId: String?, + initialMessage: Message?, + accessCode: String?, + @DrawableRes iconScanRes: Int?, + ): CompletionResult = error("This method is deprecated") + + override fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) { + // intentionally do nothing + } + + @Deprecated("TangemSdkManager shouldn't returns a string from resources") + override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { + return resources.getString(stringResId, *formatArgs) + } + + override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { + userCodeRequestPolicyInternal = policy + } + + // region Twin-specific + + override suspend fun createFirstTwinWallet( + cardId: String, + initialMessage: Message, + ): CompletionResult { + return MockProvider.createFirstTwinWallet() + } + + override suspend fun createSecondTwinWallet( + firstPublicKey: String, + firstCardId: String, + issuerKeys: KeyPair, + preparingMessage: Message, + creatingWalletMessage: Message, + initialMessage: Message, + ): CompletionResult { + return MockProvider.createSecondTwinWallet() + } + + override suspend fun finalizeTwin( + secondCardPublicKey: ByteArray, + issuerKeyPair: KeyPair, + cardId: String, + initialMessage: Message, + ): CompletionResult { + return MockProvider.finalizeTwin() + } + + // endregion +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt new file mode 100644 index 0000000000..bb8ca7deab --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.domain.sdk.mocks + +import com.tangem.common.SuccessResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +interface MockContent { + + val successResponse: SuccessResponse + + val scanResponse: ScanResponse + + val derivationTaskResponse: DerivationTaskResponse + + val cardDto: CardDTO + + val extendedPublicKey: ExtendedPublicKey + + val createProductWalletTaskResponse: CreateProductWalletTaskResponse + + // Wallet2-specific + val importWalletResponse: CreateProductWalletTaskResponse + + // Twin-specific + val finalizeTwinResponse: ScanResponse + + // Twin-specific + val createFirstTwinResponse: CreateWalletResponse + + // Twin-specific + val createSecondTwinResponse: CreateWalletResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt new file mode 100644 index 0000000000..9468912e1c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -0,0 +1,81 @@ +package com.tangem.tap.domain.sdk.mocks + +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.domain.models.scan.ProductType +import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent +import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +object MockProvider { + + private var content: MockContent = getMockContent(ProductType.Wallet) + + private var emulateError: Boolean = false + + private var emulatedError: TangemError = TangemSdkError.TagLost() + + fun setEmulateError(error: TangemError? = null) { + emulateError = true + error?.let { + emulatedError = it + } + } + + fun resetEmulateError() { + emulateError = false + } + + fun setMocks(productType: ProductType) { + content = getMockContent(productType) + } + + fun setMocks(mockContent: MockContent) { + content = mockContent + } + + fun getSuccessResponse() = CompletionResult.Success(content.successResponse).orFailure() + + fun getScanResponse() = CompletionResult.Success(content.scanResponse).orFailure() + + fun getDerivationTaskResponse() = CompletionResult.Success(content.derivationTaskResponse).orFailure() + + fun getCardDto() = CompletionResult.Success(content.cardDto).orFailure() + + fun getExtendedPublicKey() = CompletionResult.Success(content.extendedPublicKey).orFailure() + + fun getCreateProductWalletResponse(): CompletionResult { + return CompletionResult.Success(content.createProductWalletTaskResponse).orFailure() + } + + fun getImportWalletResponse(): CompletionResult { + return CompletionResult.Success(content.importWalletResponse).orFailure() + } + + // region Twin-specific + + fun finalizeTwin() = CompletionResult.Success(content.finalizeTwinResponse).orFailure() + + fun createFirstTwinWallet() = CompletionResult.Success(content.createFirstTwinResponse).orFailure() + + fun createSecondTwinWallet() = CompletionResult.Success(content.createSecondTwinResponse).orFailure() + + // endregion + + private fun getMockContent(productType: ProductType): MockContent { + return when (productType) { + ProductType.Wallet -> WalletMockContent + ProductType.Wallet2 -> Wallet2MockContent + else -> TODO() + } + } + + private fun CompletionResult.Success.orFailure(): CompletionResult { + return if (emulateError) { + CompletionResult.Failure(emulatedError) + } else { + this + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt new file mode 100644 index 0000000000..2b9a29b938 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt @@ -0,0 +1,43 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse + +object Wallet2MockContent : MockContent { + + override val scanResponse: ScanResponse + get() = TODO("Not yet implemented") + + override val cardDto: CardDTO + get() = TODO("Not yet implemented") + + override val derivationTaskResponse: DerivationTaskResponse + get() = TODO("Not yet implemented") + + override val extendedPublicKey: ExtendedPublicKey + get() = TODO("Not yet implemented") + + override val successResponse: SuccessResponse + get() = TODO("Not yet implemented") + + override val createProductWalletTaskResponse: CreateProductWalletTaskResponse + get() = TODO("Not yet implemented") + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = TODO("Not yet implemented") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt new file mode 100644 index 0000000000..8e059f5450 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -0,0 +1,247 @@ +package com.tangem.tap.domain.sdk.mocks.content + +import com.tangem.common.SuccessResponse +import com.tangem.common.card.* +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.operations.attestation.Attestation +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.tap.domain.sdk.mocks.MockContent +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import java.util.Date + +object WalletMockContent : MockContent { + + private val primaryCard = PrimaryCard( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + linkingKey = byteArrayOf( + 2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121, + -57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91, + ), + existingWalletsCount = 3, isHDWalletAllowed = true, + issuer = Card.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug), + firmwareVersion = FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + isKeysImportAllowed = false, + certificate = null, + ) + + override val cardDto = CardDTO( + cardId = "AC05000000086747", + batchId = "AC05", + cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39), + firmwareVersion = CardDTO.FirmwareVersion( + major = 4, + minor = 52, + patch = 0, + type = FirmwareVersion.FirmwareType.Release, + ), + manufacturer = CardDTO.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1649635200000), + signature = byteArrayOf(-20, 98, -101, 94, -23, 73, 122, -21, 74, 76, 79, -55, -102, -62, -30, 44, -38, 118, 75, 121, -36, 118, -62, 60, -38, -63, 33, -14, -98, -69, 112, 22, 48, -43, 47, 65, -61, -56, 38, -94, -45, 44, 95, -22, 6, -31, -40, -25, 42, 38, 94, -99, 71, 98, -33, 27, -102, 30, 52, -7, 51, -27, 84, 66), + ), + issuer = CardDTO.Issuer( + name = "TANGEM AG", + publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), + ), + settings = CardDTO.Settings( + securityDelay = 15000, + maxWalletsCount = 20, + isSettingAccessCodeAllowed = false, + isSettingPasscodeAllowed = false, + isResettingUserCodesAllowed = true, + isLinkedTerminalEnabled = true, + isBackupAllowed = true, + supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None), + isFilesAllowed = true, + isHDWalletAllowed = true, + isKeysImportAllowed = false, + ), + userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true), + linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None, + isAccessCodeSet = false, + isPasscodeSet = false, + supportedCurves = listOf( + EllipticCurve.Secp256k1, + EllipticCurve.Ed25519, + EllipticCurve.Secp256r1, + EllipticCurve.Bls12381G2, + EllipticCurve.Bls12381G2Aug, + EllipticCurve.Bls12381G2Pop, + EllipticCurve.Bip0340, + ), + wallets = listOf( + CardDTO.Wallet( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + curve = EllipticCurve.Secp256k1, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 0, + hasBackup = false, + derivedKeys = mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + ), + DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey( + publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62), + chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41), + ), + ), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14), + ), + isImported = false, + ), + CardDTO.Wallet( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + curve = EllipticCurve.Ed25519, + settings = CardWallet.Settings(isPermanent = false), + totalSignedHashes = 0, + remainingSignatures = null, + index = 1, + hasBackup = false, + derivedKeys = emptyMap(), + extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109), + chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6), + ), + isImported = false, + ), + ), + attestation = Attestation( + cardKeyAttestation = Attestation.Status.Verified, + walletKeysAttestation = Attestation.Status.Skipped, + firmwareAttestation = Attestation.Status.Skipped, + cardUniquenessAttestation = Attestation.Status.Skipped, + ), + backupStatus = CardDTO.BackupStatus.NoBackup, + ) + + override val scanResponse = ScanResponse( + card = cardDto, + productType = ProductType.Wallet, + walletData = null, + secondTwinPublicKey = null, + derivedKeys = emptyMap(), + primaryCard = null, + ) + + override val derivationTaskResponse = DerivationTaskResponse( + entries = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch + publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121), + chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge + publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124), + chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + ) + + override val extendedPublicKey = ExtendedPublicKey( + publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110), + chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ) + + override val successResponse = SuccessResponse(cardId = "AC05000000086747") + + override val createProductWalletTaskResponse = CreateProductWalletTaskResponse( + card = cardDto, + derivedKeys = mapOf( + ByteArrayKey( + byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5), + ) + to + ExtendedPublicKeysMap( + mapOf( + DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc + publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52), + chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth + publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67), + chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109), + depth = 0, + parentFingerprint = byteArrayOf(0, 0, 0, 0), + childNumber = 0, + ), + ), + ), + ), + primaryCard = primaryCard, + ) + + override val importWalletResponse: CreateProductWalletTaskResponse + get() = error("Available only for Wallet 2") + + override val createFirstTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val createSecondTwinResponse: CreateWalletResponse + get() = error("Available only for Twin") + + override val finalizeTwinResponse: ScanResponse + get() = error("Available only for Twin") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt index 681d4ffabd..00f7f53887 100644 --- a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.settings import com.tangem.domain.settings.repositories.LegacySettingsRepository -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.TangemSdkManager internal class DefaultLegacySettingsRepository( private val tangemSdkManager: TangemSdkManager, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index d229d8f05d..d1f7b8a3d6 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -3,13 +3,13 @@ package com.tangem.tap.domain.tasks.product import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.local.token.UserTokensStore import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.features.demo.DemoHelper import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 6ef5f2bbe9..34b7dada02 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -9,7 +9,7 @@ import com.tangem.common.KeyPair import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.reader.AssetReader import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.wallet.CreateWalletResponse @@ -27,8 +27,7 @@ class TwinCardsManager( private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString()) suspend fun createFirstWallet(message: Message): CompletionResult { - val response = tangemSdkManager.runTaskAsync( - runnable = CreateFirstTwinWalletTask(firstCardId), + val response = tangemSdkManager.createFirstTwinWallet( cardId = firstCardId, initialMessage = message, ) @@ -44,14 +43,15 @@ class TwinCardsManager( preparingMessage: Message, creatingWalletMessage: Message, ): CompletionResult { - val task = CreateSecondTwinWalletTask( + val response = tangemSdkManager.createSecondTwinWallet( firstPublicKey = currentCardPublicKey!!, firstCardId = firstCardId, issuerKeys = issuerKeyPair, preparingMessage = preparingMessage, creatingWalletMessage = creatingWalletMessage, + initialMessage = initialMessage, ) - val response = tangemSdkManager.runTaskAsync(task, null, initialMessage) + when (response) { is CompletionResult.Success -> { secondCardPublicKey = response.data.wallet.publicKey.toHexString() @@ -62,8 +62,9 @@ class TwinCardsManager( } suspend fun complete(message: Message): Result { - val response = tangemSdkManager.runTaskAsync( - runnable = FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair), + val response = tangemSdkManager.finalizeTwin( + secondCardPublicKey = secondCardPublicKey!!.hexToBytes(), + issuerKeyPair = issuerKeyPair, cardId = firstCardId, initialMessage = message, ) @@ -88,6 +89,7 @@ class TwinCardsManager( ) } + @Deprecated(message = "Use AssetReader instead") private fun getIssuers(reader: AssetReader): List { val file = reader.readJson(fileName = "tangem-app-config/issuers") return getAdapter().fromJson(file)!! diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt deleted file mode 100644 index ca0f135159..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/DefaultUserWalletsListManagerFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.domain.userWalletList - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles - -internal class DefaultUserWalletsListManagerFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : UserWalletsListManagerFeatureToggles { - - override val isGeneralManagerEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt deleted file mode 100644 index f94041dc73..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerFeatureTogglesModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.domain.userWalletList.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles -import com.tangem.tap.domain.userWalletList.DefaultUserWalletsListManagerFeatureToggles -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object UserWalletsListManagerFeatureTogglesModule { - - @Provides - @Singleton - fun provideUserWalletsListManagerFeatureToggles( - featureTogglesManager: FeatureTogglesManager, - ): UserWalletsListManagerFeatureToggles { - return DefaultUserWalletsListManagerFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index f2e03181e1..1128c659f5 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -66,7 +66,7 @@ internal object UserWalletsListManagerModule { val secureStorage = AndroidSecureStorage( preferences = SecureStorage.createEncryptedSharedPreferences( context = applicationContext, - storageName = USER_WALLETS_STORAGE_NAME, + storageName = "user_wallets_storage", ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt deleted file mode 100644 index 51bdd56ae6..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.tap.domain.userWalletList.di - -import android.content.Context -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import com.tangem.common.authentication.storage.AuthenticatedStorage -import com.tangem.common.json.TangemSdkAdapter -import com.tangem.common.services.secure.SecureStorage -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.sdk.storage.AndroidSecureStorage -import com.tangem.sdk.storage.createEncryptedSharedPreferences -import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager -import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager -import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager -import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator -import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository -import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository -import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository -import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository -import com.tangem.tap.domain.userWalletList.utils.json.* -import com.tangem.tap.tangemSdkManager -import com.tangem.utils.Provider - -internal const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage" - -fun UserWalletsListManager.Companion.provideBiometricImplementation( - applicationContext: Context, -): UserWalletsListManager { - val moshi = Moshi.Builder() - .add(WalletDerivedKeysMapAdapter()) - .add(ScanResponseDerivedKeysMapAdapter()) - .add(ByteArrayKeyAdapter()) - .add(ExtendedPublicKeysMapAdapter()) - .add(CardBackupStatusAdapter()) - .add(DerivationPathAdapterWithMigration()) - .add(TangemSdkAdapter.DateAdapter()) - .add(TangemSdkAdapter.DerivationNodeAdapter()) - .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model - .add(KotlinJsonAdapterFactory()) - .build() - - val secureStorage = AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = USER_WALLETS_STORAGE_NAME, - ), - ) - - val authenticatedStorage = AuthenticatedStorage( - secureStorage = UserWalletsKeysStoreDecorator( - featureStorage = secureStorage, - cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, - ), - keystoreManager = DelegatedKeystoreManager( - keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, - ), - ) - - val keysRepository = BiometricUserWalletsKeysRepository( - moshi = moshi, - secureStorage = secureStorage, - authenticatedStorage = authenticatedStorage, - ) - val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( - moshi = moshi, - secureStorage = secureStorage, - ) - val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository( - moshi = moshi, - secureStorage = secureStorage, - ) - val selectedUserWalletRepository = DefaultSelectedUserWalletRepository( - secureStorage = secureStorage, - ) - - return BiometricUserWalletsListManager( - keysRepository = keysRepository, - publicInformationRepository = publicInformationRepository, - sensitiveInformationRepository = sensitiveInformationRepository, - selectedUserWalletRepository = selectedUserWalletRepository, - ) -} - -fun UserWalletsListManager.Companion.provideRuntimeImplementation(): UserWalletsListManager { - return RuntimeUserWalletsListManager() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 99418a7aa0..52e9be2c6d 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -12,6 +12,7 @@ import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository import com.tangem.tap.domain.userWalletList.utils.encryptionKey +import com.tangem.tap.domain.userWalletList.utils.lockAll import com.tangem.tap.domain.userWalletList.utils.toUserWallets import com.tangem.tap.domain.userWalletList.utils.updateWith import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -27,11 +28,16 @@ internal class BiometricUserWalletsListManager( ) : UserWalletsListManager.Lockable { private val state = MutableStateFlow(State()) + override val isLockable: Boolean = true + override val userWallets: Flow> get() = state .mapLatest { it.userWallets } .distinctUntilChanged() + override val userWalletsSync: List + get() = state.value.userWallets + override val selectedUserWallet: Flow get() = state .mapLatest { state -> @@ -78,11 +84,13 @@ internal class BiometricUserWalletsListManager( } override fun lock() { - state.update { State() } - } - - override fun isLockable(): Boolean { - return true + state.update { prevState -> + prevState.copy( + encryptionKeys = emptyList(), + userWallets = prevState.userWallets.lockAll(), + isLocked = true, + ) + } } override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index 762ba82f54..4565c111ed 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -33,31 +33,52 @@ internal class GeneralUserWalletsListManager( ) : UserWalletsListManager.Lockable { private val applicationScope = CoroutineScope(dispatchers.io) - private val implementation = MutableStateFlow(runtimeUserWalletsListManager) + private val implementation: MutableStateFlow = MutableStateFlow(value = null) + + private val requireImplementation: UserWalletsListManager + get() = requireNotNull(implementation.value) { + "UserWalletsListManager is not initialized" + } init { subscribeOnCurrentManager() } + override val isLockable: Boolean + get() = requireImplementation.isLockable + override val userWallets: Flow> - get() = implementation.flatMapLatest { it.userWallets } + get() = implementation.transformLatest { impl -> + if (impl != null && impl.hasUserWallets) { + emitAll(impl.userWallets) + } + } + + override val userWalletsSync: List + get() = requireImplementation.userWalletsSync override val selectedUserWallet: Flow - get() = implementation.flatMapLatest { it.selectedUserWallet } + get() = implementation.transformLatest { impl -> + if (impl != null && impl.hasUserWallets) { + emitAll(impl.selectedUserWallet) + } + } override val selectedUserWalletSync: UserWallet? - get() = implementation.value.selectedUserWalletSync + get() = requireImplementation.selectedUserWalletSync override val hasUserWallets: Boolean - get() = implementation.value.hasUserWallets + get() = requireImplementation.hasUserWallets override val walletsCount: Int - get() = implementation.value.walletsCount + get() = requireImplementation.walletsCount override val isLocked: Flow - get() = implementation.flatMapLatest { - if (it is UserWalletsListManager.Lockable) { - it.isLocked + get() = implementation.transformLatest { impl -> + if (impl == null) return@transformLatest + + if (impl is UserWalletsListManager.Lockable) { + emitAll(impl.isLocked) } else { error("RuntimeUserWalletsListManager is not lockable") } @@ -65,43 +86,45 @@ internal class GeneralUserWalletsListManager( override val isLockedSync: Boolean get() { - val implementation = implementation.value - return if (implementation is UserWalletsListManager.Lockable) { - implementation.isLockedSync + val impl = requireImplementation + + return if (impl is UserWalletsListManager.Lockable) { + impl.isLockedSync } else { error("RuntimeUserWalletsListManager is not lockable") } } override suspend fun select(userWalletId: UserWalletId): CompletionResult { - return implementation.value.select(userWalletId) + return requireImplementation.select(userWalletId) } override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { - return implementation.value.save(userWallet, canOverride) + return requireImplementation.save(userWallet, canOverride) } override suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, ): CompletionResult { - return implementation.value.update(userWalletId, update) + return requireImplementation.update(userWalletId, update) } override suspend fun delete(userWalletIds: List): CompletionResult { - return implementation.value.delete(userWalletIds) + return requireImplementation.delete(userWalletIds) } override suspend fun clear(): CompletionResult { - return implementation.value.clear() + return requireImplementation.clear() } override suspend fun get(userWalletId: UserWalletId): CompletionResult { - return implementation.value.get(userWalletId) + return requireImplementation.get(userWalletId) } override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult { - val implementation = implementation.value + val implementation = requireImplementation + return if (implementation is UserWalletsListManager.Lockable) { implementation.unlock(type) } else { @@ -110,7 +133,8 @@ internal class GeneralUserWalletsListManager( } override fun lock() { - val implementation = implementation.value + val implementation = requireImplementation + return if (implementation is UserWalletsListManager.Lockable) { implementation.lock() } else { @@ -118,10 +142,6 @@ internal class GeneralUserWalletsListManager( } } - override fun isLockable(): Boolean { - return implementation.value.isLockable() - } - private fun subscribeOnCurrentManager() { appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) .distinctUntilChanged() @@ -144,17 +164,17 @@ internal class GeneralUserWalletsListManager( destinationManager = possibleManager, ) - previousManager.clear() + previousManager?.clear() } .flowOn(dispatchers.io) .launchIn(applicationScope) } private suspend fun copySelectedUserWallet( - sourceManager: UserWalletsListManager, + sourceManager: UserWalletsListManager?, destinationManager: UserWalletsListManager, ): UserWalletsListManager { - sourceManager.selectedUserWalletSync?.let { selectedWallet -> + sourceManager?.selectedUserWalletSync?.let { selectedWallet -> destinationManager.save(selectedWallet, canOverride = true) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt index 31ef3366dd..4d211d9b88 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt @@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.* internal class RuntimeUserWalletsListManager : UserWalletsListManager { private val state = MutableStateFlow(State()) + override val isLockable: Boolean = false + override val userWallets: Flow> get() = state .mapLatest { listOfNotNull(it.userWallet) } @@ -24,6 +26,9 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { .filterNotNull() .distinctUntilChanged() + override val userWalletsSync: List + get() = listOfNotNull(state.value.userWallet) + override val selectedUserWalletSync: UserWallet? get() = state.value.userWallet @@ -34,7 +39,7 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { * only 1 wallet stored in runtime implementation */ override val walletsCount: Int - get() = 1 + get() = if (hasUserWallets) 1 else 0 override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { state.value.userWallet @@ -85,10 +90,6 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager { state.value.userWallet ?: walletNotFound() } - override fun isLockable(): Boolean { - return false - } - private fun saveInternal(userWallet: UserWallet): CompletionResult = catching { state.update { prevState -> prevState.copy( diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index 451e717e9e..7b869eb69f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -61,4 +61,14 @@ internal fun List.updateWith( ?: wallet } } -} \ No newline at end of file +} + +internal fun List.lockAll(): List = map(UserWallet::lock) + +internal fun UserWallet.lock(): UserWallet = copy( + scanResponse = scanResponse.copy( + card = scanResponse.card.copy( + wallets = emptyList(), + ), + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt index 16a35bf539..2b3094d9f6 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/BnbHelper.kt @@ -10,13 +10,11 @@ import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTrans import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData import com.tangem.tap.features.details.redux.walletconnect.TradeData -import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder import timber.log.Timber internal object BnbHelper { - fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer { + fun createMessageData(order: WcBinanceTransferOrder): BinanceMessageData.Transfer { val input = order.msgs.first().inputs.first() val output = order.msgs.first().inputs.first() @@ -42,51 +40,7 @@ internal object BnbHelper { ) } - fun WcBinanceTradeOrder.toWCBinanceTradeOrder(): WCBinanceTradeOrder { - return WCBinanceTradeOrder( - account_number = accountNumber, - chain_id = chainId, - data = data, - memo = memo, - sequence = sequence, - source = source, - msgs = msgs.map { it.toWCBinanceTradeOrderMessage() }, - ) - } - - private fun WcBinanceTradeOrder.Message.toWCBinanceTradeOrderMessage(): WCBinanceTradeOrder.Message { - return WCBinanceTradeOrder.Message(id, orderType, price, quantity, sender, side, symbol, timeInforce) - } - - fun WcBinanceTransferOrder.toWCBinanceTransferOrder(): WCBinanceTransferOrder { - return WCBinanceTransferOrder( - account_number = accountNumber, - chain_id = chainId, - data = data, - memo = memo, - sequence = sequence, - source = source, - msgs = msgs.map { it.toWCBinanceTransferOrderMessage() }, - ) - } - - private fun WcBinanceTransferOrder.Message.toWCBinanceTransferOrderMessage(): WCBinanceTransferOrder.Message { - return WCBinanceTransferOrder.Message( - inputs.map { it.toWCBinanceItem() }, - outputs.map { it.toWCBinanceItem() }, - ) - } - - private fun WcBinanceTransferOrder.Message.Item.toWCBinanceItem(): WCBinanceTransferOrder.Message.Item { - return WCBinanceTransferOrder.Message.Item( - address, - coins.map { - WCBinanceTransferOrder.Message.Item.Coin(it.amount, it.denom) - }, - ) - } - - fun createMessageData(order: WCBinanceTradeOrder): BinanceMessageData.Trade { + fun createMessageData(order: WcBinanceTradeOrder): BinanceMessageData.Trade { val address = order.msgs.first().sender val tradeData = order.msgs.map { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt deleted file mode 100644 index 9078975c2c..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ /dev/null @@ -1,562 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.EllipticCurve -import com.tangem.common.extensions.guard -import com.tangem.datasource.api.common.createNetworkLoggingInterceptor -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.walletconnect.extensions.isDappSupported -import com.tangem.tap.domain.walletconnect.extensions.toWcEthereumSignMessage -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction -import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData -import com.tangem.tap.features.details.redux.walletconnect.* -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.walletConnectRepository -import com.trustwallet.walletconnect.WCClient -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.binance.WCBinanceCancelOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder -import com.trustwallet.walletconnect.models.binance.WCBinanceTxConfirmParam -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage -import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction -import com.trustwallet.walletconnect.models.session.WCAddNetwork -import com.trustwallet.walletconnect.models.session.WCSession -import com.trustwallet.walletconnect.models.session.WCSessionUpdate -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import timber.log.Timber -import java.util.UUID -import java.util.concurrent.TimeUnit -import kotlin.collections.set - -@Suppress("LargeClass") -internal class WalletConnectManager { - - private var cardId: String? = null - - private val okHttpClient: OkHttpClient by lazy { - OkHttpClient.Builder() - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(20, TimeUnit.SECONDS) - .writeTimeout(20, TimeUnit.SECONDS) - .addInterceptor(interceptor) - .addInterceptor(RetryInterceptor()) - .build() - } - - private val interceptor by lazy { - createNetworkLoggingInterceptor() - } - - private var sessions: MutableMap = mutableMapOf() - - fun connect(wcUri: String) { - val session = WCSession.from(wcUri).guard { - store.dispatchOnMain( - WalletConnectAction.FailureEstablishingSession( - session = null, - error = TapError.WalletConnect.UnsupportedLink, - ), - ) - return - } - if (sessions[session.topic] != null) { - store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession) - return - } - val client = WCClient(httpClient = okHttpClient) - setListeners(client) - val peerId = UUID.randomUUID().toString() - - try { - client.connect(session, tangemPeerMeta, peerId) - } catch (exception: IllegalArgumentException) { - store.dispatchOnMain( - WalletConnectAction.FailureEstablishingSession( - session = null, - error = TapError.WalletConnect.UnsupportedLink, - ), - ) - return - } - - sessions[session.topic] = WalletConnectActiveData( - peerId = peerId, - remotePeerId = null, - session = session, - client = client, - wallet = WalletForSession(), - ) - setupConnectionTimeoutCheck(session) - } - - fun updateSession(session: WalletConnectSession) { - val updatedSession = sessions[session.session.topic]?.copy( - wallet = session.wallet, - ) - if (updatedSession != null) { - sessions[session.session.topic] = updatedSession - } - } - - fun updateBlockchain(session: WalletConnectSession) { - sessions[session.session.topic]?.client?.updateSession( - accounts = listOfNotNull(session.getAddress()), - chainId = session.wallet.blockchain?.getChainId(), - approved = true, - ) - - val updatedSession = sessions[session.session.topic]?.copy( - wallet = session.wallet, - ) - if (updatedSession != null) { - sessions[session.session.topic] = updatedSession - } - } - - @Suppress("MagicNumber") - private fun setupConnectionTimeoutCheck(session: WCSession) { - scope.launch { - delay(20_000) - val data = sessions[session.topic] - if (data != null && data.peerMeta == null) { - disconnect(session) - store.dispatchOnMain(WalletConnectAction.OpeningSessionTimeout(session)) - } - } - } - - fun restoreSessions(scanResponse: ScanResponse) { - val walletPublicKey = scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }?.publicKey - ?: return - if (scanResponse.card.backupStatus?.isActive != true) cardId = scanResponse.card.cardId - val sessions = walletConnectRepository.loadSavedSessions() - // filter sessions for this particular card - .filter { it.wallet.walletPublicKey.contentEquals(walletPublicKey) } - this.sessions = sessions - .map { session -> - WalletConnectActiveData( - peerId = session.peerId, - remotePeerId = session.remotePeerId, - client = WCClient(httpClient = okHttpClient), - session = session.session, - peerMeta = session.peerMeta, - wallet = session.wallet, - ) - .also { - setListeners(it.client) - it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId) - } - }.associateBy { it.session.topic }.toMutableMap() - - store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions)) - } - - fun approve(session: WCSession) { - val activeData = sessions[session.topic] ?: return - removeSimilarSessions(activeData) - - val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey ?: return - val blockchain = activeData.wallet.getBlockchainForSession() - val accounts = listOf(blockchain.makeAddresses(key).first().value) - val approved = activeData.client.approveSession( - accounts = accounts, - chainId = blockchain.getChainId() ?: Blockchain.Ethereum.getChainId()!!, - ) - if (approved) { - val walletConnectSession = WalletConnectSession( - peerId = activeData.peerId, - remotePeerId = activeData.remotePeerId, - wallet = activeData.wallet, - session = session, - peerMeta = activeData.peerMeta!!, - ) - walletConnectRepository.saveSession(walletConnectSession) - store.dispatchOnMain( - WalletConnectAction.ApproveSession.Success( - walletConnectSession, - ), - ) - } - } - - private fun removeSimilarSessions(activeData: WalletConnectActiveData) { - val sessionsToRemove = sessions.filter { - it.value.wallet.walletPublicKey?.equals(activeData.wallet.walletPublicKey) == true && - it.value.peerMeta?.url == activeData.peerMeta?.url && - it.value.session != activeData.session - } - Timber.d("RemoveSimilarSessions: ${sessionsToRemove.values.map { it.client.session }}") - sessionsToRemove.forEach { disconnect(it.value.session) } - } - - fun rejectRequest(topic: String, id: Long) { - val activeData = sessions[topic] ?: return - activeData.client.rejectRequest(id) - } - - private fun acceptRequest(topic: String, id: Long, data: String) { - val activeData = sessions[topic] ?: return - activeData.client.approveRequest(id, data) - } - - fun disconnect(session: WCSession) { - val activeData = sessions[session.topic] ?: return - val disconnected = if (activeData.client.isConnected) { - activeData.client.killSession() - } else { - true - } - - if (disconnected) { - onSessionClosed(session) - } - } - - private fun onSessionClosed(session: WCSession) { - sessions.remove(session.topic) - walletConnectRepository.removeSession(session) - store.dispatchOnMain(WalletConnectAction.RemoveSession(session)) - } - - fun handleTransactionRequest( - transaction: WcEthereumTransaction, - session: WalletConnectSession, - id: Long, - type: WcEthTransactionType, - ) { - val activeData = sessions[session.session.topic] ?: return - scope.launch { - val data = WalletConnectSdkHelper().prepareTransactionData( - EthTransactionData( - transaction = transaction, - networkId = session.wallet.blockchain?.toNetworkId() ?: "", - rawDerivationPath = session.wallet.derivationPath?.rawPath, - id = id, - topic = session.session.topic, - type = type, - metaName = session.peerMeta.name, - metaUrl = session.peerMeta.url, - ), - ).guard { - sessions[session.session.topic] = activeData.copy(transactionData = null) - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - session.session.topic, - id, - ), - ) - return@launch - } - sessions[session.session.topic] = activeData.copy(transactionData = data) - - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.RequestTransaction( - WcPreparedRequest.EthTransaction( - preparedRequestData = data, - topic = session.session.topic, - requestId = id, - derivationPath = data.walletManager.wallet.publicKey.derivationPath?.rawPath, - ), - ), - ), - ) - } - } - - fun completeTransaction(topic: Topic) { - val activeData = sessions[topic] - val data = activeData?.transactionData ?: return - scope.launch { - val hash = WalletConnectSdkHelper().completeTransaction(data, cardId).guard { - sessions[topic] = activeData.copy(transactionData = null) - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - topic, - data.id, - ), - ) - return@launch - } - acceptRequest(topic, data.id, hash) - sessions[topic] = activeData.copy(transactionData = null) - } - } - - fun signBnb(id: Long, data: ByteArray, topic: Topic) { - val activeData = sessions[topic] ?: return - scope.launch { - val hash = WalletConnectSdkHelper().signBnbTransaction( - data = data, - networkId = activeData.wallet.blockchain?.toNetworkId() ?: "", - derivationPath = activeData.wallet.derivationPath?.rawPath, - cardId = cardId, - ).guard { - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - topic, - id, - ), - ) - return@launch - } - acceptRequest(topic, id, hash) - } - } - - fun handlePersonalSignRequest(message: WCEthereumSignMessage, session: WalletConnectSession, id: Long) { - val activeData = sessions[session.session.topic] ?: return - scope.launch { - val data = WalletConnectSdkHelper().prepareDataForPersonalSign( - message = message.toWcEthereumSignMessage(), - topic = session.session.topic, - id = id, - metaName = session.peerMeta.name, - ) - sessions[session.session.topic] = activeData.copy(personalSignData = data) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.PersonalSign( - WcPreparedRequest.EthSign( - preparedRequestData = data, - topic = session.session.topic, - requestId = id, - derivationPath = session.wallet.derivationPath?.rawPath, - ), - ), - ), - ) - } - } - - fun sendSignedMessage(topic: Topic) { - val activeData = sessions[topic] - val data = activeData?.personalSignData ?: return - scope.launch { - val hash = WalletConnectSdkHelper().signPersonalMessage( - hashToSign = data.hash, - networkId = activeData.wallet.blockchain?.toNetworkId() ?: "", - type = data.type, - derivationPath = activeData.wallet.derivationPath?.rawPath, - cardId = cardId, - ) - .guard { - sessions[topic] = activeData.copy(transactionData = null) - store.dispatchOnMain( - WalletConnectAction.RejectRequest( - topic, - data.id, - ), - ) - return@launch - } - sessions[topic] = activeData.copy(personalSignData = data) - acceptRequest(topic, data.id, hash) - sessions[topic] = activeData.copy(transactionData = null) - } - } - - @Suppress("LongMethod", "ComplexMethod") - private fun setListeners(client: WCClient) { - client.onSessionRequest = { id: Long, peer: WCPeerMeta -> - Timber.d("OnSessionRequest: $peer") - val session = client.session - val data = sessions[session?.topic]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId) - if (data != null && session != null) { - if (!peer.isDappSupported()) { - store.dispatchOnMain( - WalletConnectAction.FailureEstablishingSession( - session = session, - error = TapError.WalletConnect.UnsupportedDapp, - ), - ) - } else { - sessions[session.topic] = data - val sessionData = data.toWalletConnectSession() - sessionData?.let { - store.dispatchOnMain( - WalletConnectAction.ScanCard( - session = sessionData, - chainId = client.chainId?.toIntOrNull(), - ), - ) - } - } - } - } - client.onSessionUpdate = { id: Long, update: WCSessionUpdate -> - Timber.d("onSessionUpdate: $update") - val session = client.session - if (session != null && !update.approved) onSessionClosed(session) - } - client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction -> - Timber.d("onEthSendTransaction: $transaction") - // Analytics.logWcEvent( - // AnalyticsAnOld.WcAnalyticsEvent.Action( - // AnalyticsAnOld.WcAction.SendTransaction - // ) - // ) - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.HandleTransactionRequest( - transaction = transaction, - session = sessionData, - id = id, - type = WcEthTransactionType.EthSendTransaction, - ), - ) - } - } - client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction -> - Timber.d("onEthSignTransaction: $transaction") - // Analytics.logWcEvent( - // AnalyticsAnOld.WcAnalyticsEvent.Action( - // AnalyticsAnOld.WcAction.SignTransaction - // ) - // ) - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.HandleTransactionRequest( - transaction = transaction, - session = sessionData, - id = id, - type = WcEthTransactionType.EthSignTransaction, - ), - ) - } - } - client.onEthSign = { id: Long, message: WCEthereumSignMessage -> - Timber.d("onEthSign: $message") - // Analytics.logWcEvent( - // AnalyticsAnOld.WcAnalyticsEvent.Action( - // AnalyticsAnOld.WcAction.PersonalSign - // ) - // ) - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.HandlePersonalSignRequest( - message, - sessionData, - id, - ), - ) - } - } - - client.onBnbCancel = { id: Long, order: WCBinanceCancelOrder -> - } - client.onBnbTrade = { id: Long, order: WCBinanceTradeOrder -> - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.BinanceTransaction.Trade( - id = id, - order = order, - sessionData = sessionData, - ), - ) - } - } - client.onBnbTransfer = { id: Long, order: WCBinanceTransferOrder -> - sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData -> - store.dispatchOnMain( - WalletConnectAction.BinanceTransaction.Transfer( - id = id, - order = order, - sessionData = sessionData, - ), - ) - } - } - client.onBnbTxConfirm = { id: Long, order: WCBinanceTxConfirmParam -> - // send empty approve request if status is OK - if (order.ok) client.approveRequest(id, "") - } - client.onDisconnect = { code: Int, reason: String -> - val session = client.session - if (session != null) { - onSessionClosed(session) - } - } - client.onWalletChangeNetwork = { id: Long, chainId: Int -> - switchChain(chainId, client) - } - client.onWalletAddNetwork = { id: Long, network: WCAddNetwork -> - // TODO - // In fact this method is used to add a EVM network. It provides RPC url and chain ID. - // Here now it just tries to switch to a EVM network with a provided chain ID. - try { - val chainId = Integer.decode(network.chainIdHex) - switchChain(chainId, client) - } catch (exception: Exception) { - Timber.d("WC add network error: chain could not be parsed") - } - } - } - - private fun switchChain(chainId: Int, client: WCClient) { - val blockchain = Blockchain.fromChainId(chainId) - Timber.d("WC switch chainID\nNew Blockchain: $blockchain") - val session = sessions[client.session?.topic]?.toWalletConnectSession() - if (session != null) { - store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session)) - } - } - - companion object { - const val WC_SCHEME = "wc" - - private val tangemPeerMeta = WCPeerMeta(name = "Tangem Wallet", url = "https://tangem.com") - - fun isCorrectWcUri(string: String): Boolean = WCSession.from(string) != null - } -} - -internal typealias Topic = String - -internal data class WalletConnectActiveData( - val peerId: String, - val remotePeerId: String?, - val client: WCClient, - val session: WCSession, - val peerMeta: WCPeerMeta? = null, - val wallet: WalletForSession, - val transactionData: WcTransactionData? = null, - val personalSignData: WcPersonalSignData? = null, -) { - fun toWalletConnectSession(): WalletConnectSession? { - if (peerMeta == null) return null - return WalletConnectSession( - peerId = peerId, - remotePeerId = remotePeerId, - wallet = wallet, - session = session, - peerMeta = peerMeta, - ) - } -} - -@Suppress("MagicNumber") -internal class RetryInterceptor : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request: Request = chain.request() - val response = chain.proceed(request) - when (response.code) { - 502 -> { - return chain.proceed(request) - } - } - return response - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt deleted file mode 100644 index 19e2e44e42..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectNetworkUtils.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import com.tangem.blockchain.common.Blockchain -import com.trustwallet.walletconnect.models.WCPeerMeta - -object WalletConnectNetworkUtils { - fun parseBlockchain(chainId: Int?, peer: WCPeerMeta): Blockchain? { - return when { - peer.url.contains("pancakeswap.finance") -> { - Blockchain.BSC - } - peer.url.contains("optimism") -> { - Blockchain.Optimism - } - chainId != null -> { - Blockchain.fromChainId(chainId) - } - peer.url.contains("matic.network") || peer.name == "Polygon" -> { - Blockchain.Polygon - } - peer.url.contains("binance.org") || peer.name.contains("Binance") -> { - if (peer.icons.firstOrNull()?.contains("testnet") == true) { - Blockchain.BinanceTestnet - } else { - Blockchain.Binance - } - } - peer.name.contains("BSC") -> { - Blockchain.BSC - } - peer.url.contains("honeyswap.1hive.eth.limo") -> { - // Check if something's changed after this bug report: - // https://github.com/1Hive/honeyswap-interface/issues/83 - Blockchain.Gnosis - } - else -> { - Blockchain.Ethereum - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt deleted file mode 100644 index de77928810..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.tap.domain.walletconnect - -import android.app.Application -import android.content.Context -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Types -import com.tangem.common.extensions.hexToBytes -import com.tangem.common.extensions.toHexString -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.features.details.redux.walletconnect.WalletForSession -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.session.WCSession -import timber.log.Timber -import java.io.FileNotFoundException -import java.nio.charset.Charset - -class WalletConnectRepository(val context: Application) { - private val walletConnectAdapter: JsonAdapter> = MoshiConverter.sdkMoshi.adapter( - Types.newParameterizedType(List::class.java, SessionDao::class.java), - ) - - fun saveSession(session: WalletConnectSession) { - val sessions = loadSavedSessions() + session - saveSessions(sessions) - } - - fun removeSession(session: WCSession) { - val sessions = loadSavedSessions().filterNot { it.session == session } - saveSessions(sessions) - } - - fun loadSavedSessions(): List { - return try { - val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS) - .hexToUtf8() - walletConnectAdapter.fromJson(json)!!.map { it.toSession() } - } catch (e: FileNotFoundException) { - emptyList() - } catch (exception: Exception) { - Timber.w(exception) - emptyList() - } - } - - private fun saveSessions(sessions: List) { - val json = walletConnectAdapter.toJson(sessions.map { SessionDao.fromSession(it) }) - .utf8ToHex() // convert to hex to solve problems with saving text with emojis - Timber.e("WC sessions, saving following json: $json") - context.rewriteFile(json, FILE_NAME_PREFIX_SESSIONS) - } - - private fun String.utf8ToHex(): String { - return this.toByteArray().toHexString() - } - - private fun String.hexToUtf8(): String { - return this.hexToBytes().toString(Charset.defaultCharset()) - } - - private fun Context.readFileText(fileName: String): String = - this.openFileInput(fileName).bufferedReader().readText() - - private fun Context.rewriteFile(content: String, fileName: String) { - this.openFileOutput(fileName, Context.MODE_PRIVATE).use { - it.write(content.toByteArray(), 0, content.length) - } - } - - companion object { - private const val FILE_NAME_PREFIX_SESSIONS = "wc_sessions" - } -} - -@JsonClass(generateAdapter = true) -data class SessionDao( - val peerId: String, - val remotePeerId: String?, - val wallet: WalletForSession, - val session: WCSession, - val peerMeta: WCPeerMeta, -) { - fun toSession(): WalletConnectSession { - return WalletConnectSession( - peerId = peerId, - remotePeerId = remotePeerId, - wallet = wallet, - session = session, - peerMeta = peerMeta, - ) - } - - companion object { - fun fromSession(session: WalletConnectSession): SessionDao { - return SessionDao( - peerId = session.peerId, - remotePeerId = session.remotePeerId, - wallet = session.wallet, - session = session.session, - peerMeta = session.peerMeta, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 12dc242d80..2dfd015a7e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -8,6 +8,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.* +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.CompletionResult import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey @@ -15,13 +16,10 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString -import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder -import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder import com.tangem.tap.domain.walletconnect2.domain.TransactionType import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage @@ -38,7 +36,6 @@ import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionReque import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userWalletsListManager import timber.log.Timber import java.math.BigDecimal import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam @@ -46,20 +43,35 @@ import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam @Suppress("LargeClass") class WalletConnectSdkHelper { + private val userWalletsListManager by lazy { + store.inject(DaggerGraphState::generalUserWalletsListManager) + } + @Suppress("MagicNumber") - suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData? { + suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData { val transaction = data.transaction - val blockchain = Blockchain.fromNetworkId(data.networkId) ?: return null - val walletManager = getWalletManager(blockchain, data.rawDerivationPath) ?: return null + val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) { + "Blockchain not found" + } + val walletManager = requireNotNull(getWalletManager(blockchain, data.rawDerivationPath)) { + "WalletManager not found" + } walletManager.safeUpdate(isDemoCard()) val wallet = walletManager.wallet - val balance = wallet.amounts[AmountType.Coin]?.value ?: return null + val balance = requireNotNull(wallet.amounts[AmountType.Coin]?.value) { + "Coin balance not found" + } val decimals = wallet.blockchain.decimals() - val value = (transaction.value ?: "0").hexToBigDecimal() - .movePointLeft(decimals) ?: return null + val value = (transaction.value ?: "0") + .hexToBigDecimal() + .movePointLeft(decimals) + + requireNotNull(value) { + "Transaction amount is null" + } val gasLimit = getGasLimitFromTx(value, walletManager, transaction) @@ -68,20 +80,23 @@ class WalletConnectSdkHelper { is Result.Success -> result.data.toBigDecimal() is Result.Failure -> { (result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") } - return null + + error("Unable to get gas price: ${result.error}") } - null -> return null + null -> error("Gas price is null") } val fee = (gasLimit * gasPrice).movePointLeft(decimals) val total = value + fee + val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" } + val transactionData = TransactionData( amount = Amount(value, wallet.blockchain), // TODO refactoring fee = Fee.Common(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, - destinationAddress = transaction.to!!, + destinationAddress = destinationAddress, extras = EthereumTransactionExtras( data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(), gasLimit = gasLimit.toBigInteger(), @@ -101,6 +116,7 @@ class WalletConnectSdkHelper { id = data.id, type = data.type, ) + return WcTransactionData( type = data.type, transaction = transactionData, @@ -227,11 +243,11 @@ class WalletConnectSdkHelper { } fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade { - return BnbHelper.createMessageData(data.toWCBinanceTradeOrder()) + return BnbHelper.createMessageData(data) } fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer { - return BnbHelper.createMessageData(data.toWCBinanceTransferOrder()) + return BnbHelper.createMessageData(data) } suspend fun signBnbTransaction( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt deleted file mode 100644 index d227a42923..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/extensions/WcPeerMeta.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.domain.walletconnect.extensions - -import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction -import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage -import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction - -internal fun WCPeerMeta.isDappSupported(): Boolean { - return !unsupportedDappsList.any { this.url.contains(it) } -} - -private val unsupportedDappsList: List = listOf("dydx.exchange") - -internal fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction { - return WcEthereumTransaction( - from = from, - to = to, - nonce = nonce, - gasPrice = gasPrice, - maxFeePerGas = maxFeePerGas, - maxPriorityFeePerGas = maxPriorityFeePerGas, - gas = gas, - gasLimit = gasLimit, - value = value, - data = data, - ) -} - -internal fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcSignMessage { - return WcSignMessage( - raw = raw, - type = when (type) { - WCEthereumSignMessage.WCSignType.MESSAGE -> WcSignMessage.WCSignType.MESSAGE - WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcSignMessage.WCSignType.PERSONAL_MESSAGE - WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcSignMessage.WCSignType.TYPED_MESSAGE - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index 63e68ba973..edddd1a886 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -1,13 +1,13 @@ package com.tangem.tap.domain.walletconnect2.app import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles internal class TangemWcBlockchainHelper( - private val featureToggles: WalletConnectFeatureToggles, + featureToggles: WalletConnectFeatureToggles, ) : WcBlockchainHelper { private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt index 0fb1675a82..a5b2590fdf 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectRepository.kt @@ -375,6 +375,7 @@ internal class DefaultWalletConnectRepository( override fun rejectRequest(requestData: RequestData, error: WalletConnectError) { val session = currentSessions.find { it.topic == requestData.topic } + analyticsHandler.send( WalletConnect.RequestHandled( WalletConnect.RequestHandledParams( @@ -386,17 +387,18 @@ internal class DefaultWalletConnectRepository( ), ), ) - cancelRequest(requestData.topic, requestData.requestId) + + cancelRequest(requestData.topic, requestData.requestId, error.error) } - override fun cancelRequest(topic: String, id: Long) { + override fun cancelRequest(topic: String, id: Long, message: String) { Web3Wallet.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( sessionTopic = topic, jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( id = id, code = 0, - message = "", + message = message, ), ), onSuccess = {}, @@ -407,8 +409,8 @@ internal class DefaultWalletConnectRepository( override fun reject() { Web3Wallet.rejectSession( params = Wallet.Params.SessionReject( - sessionProposal?.proposerPublicKey ?: "", - "", + proposerPublicKey = sessionProposal?.proposerPublicKey ?: "", + reason = "", ), onSuccess = { Timber.d("Rejected successfully: $it") diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 927eab61da..faaa951d2e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -6,6 +6,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.files.FileReader +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl @@ -35,6 +38,9 @@ internal object WalletConnectInteractorModule { wcRepository: WalletConnectRepository, wcSessionsRepository: WalletConnectSessionsRepository, walletConnectFeatureToggles: WalletConnectFeatureToggles, + currenciesRepository: CurrenciesRepository, + walletManagersFacade: WalletManagersFacade, + userWalletsListManager: UserWalletsListManager, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), @@ -42,7 +48,10 @@ internal object WalletConnectInteractorModule { sessionsRepository = wcSessionsRepository, sdkHelper = WalletConnectSdkHelper(), blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles), - dispatcher = AppCoroutineDispatcherProvider(), + currenciesRepository = currenciesRepository, + walletManagersFacade = walletManagersFacade, + userWalletsListManager = userWalletsListManager, + dispatchers = AppCoroutineDispatcherProvider(), ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 3bf99ffd83..187f705e63 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -1,26 +1,52 @@ package com.tangem.tap.domain.walletconnect2.domain +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.onEach +import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +@Suppress("LargeClass", "LongParameterList") class WalletConnectInteractor( private val handler: WalletConnectEventsHandler, private val walletConnectRepository: WalletConnectRepository, private val sessionsRepository: WalletConnectSessionsRepository, private val sdkHelper: WalletConnectSdkHelper, - private val dispatcher: CoroutineDispatcherProvider, + private val dispatchers: CoroutineDispatcherProvider, + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val userWalletsListManager: UserWalletsListManager, val blockchainHelper: WcBlockchainHelper, ) { + private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { + GetSelectedWalletUseCase(userWalletsListManager) + } + + private val wcScope = CoroutineScope( + Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("wcScope"), + ) + + private val listenerScope = CoroutineScope( + Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("listenScope"), + ) + private val events = walletConnectRepository.events private val sessions = walletConnectRepository.activeSessions @@ -34,19 +60,56 @@ class WalletConnectInteractor( sdkHelper = sdkHelper, ) - suspend fun startListening(userWalletId: String, cardId: String?) { + init { + getSelectedWalletUseCase().onRight { userWalletFlow -> + userWalletFlow + .conflate() + .distinctUntilChanged() + .onEach(::initWithWallet) + .flowOn(dispatchers.io) + .launchIn(wcScope) + } + } + + private suspend fun initWithWallet(userWallet: UserWallet) { + if (userWallet.isMultiCurrency) { + Timber.d("WalletConnect: initialize and setup networks for ${userWallet.walletId}") + startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet)) + subscribeOnCurrenciesUpdates(userWallet) + } + } + + private fun subscribeOnCurrenciesUpdates(userWallet: UserWallet) { + currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWallet.walletId) + .conflate() + .distinctUntilChanged() + .onEach { currencies -> + setupUserChains(userWallet, currencies) + } + .flowOn(dispatchers.io) + .launchIn(wcScope) + } + + private suspend fun setupUserChains(userWallet: UserWallet, currencies: List) { + val accounts = getAccountsForWc( + userWallet = userWallet, + networks = currencies.map { it.network }, + ) + setUserChains(accounts) + } + + private suspend fun startListeningWc(userWalletId: String, cardId: String?) { this.userWalletId = userWalletId this.cardId = cardId - - coroutineScope { + listenerScope.coroutineContext.cancelChildren() + listenerScope.launch { launch { subscribeToEvents() } launch { subscribeToSessions() } - walletConnectRepository.updateSessions() } } - fun setUserChains(accounts: List) { + private fun setUserChains(accounts: List) { val userNamespaces: Map> = accounts .groupBy { account -> blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) @@ -106,7 +169,7 @@ class WalletConnectInteractor( } } } - .flowOn(dispatcher.io) + .flowOn(dispatchers.io) .collect() } @@ -117,7 +180,7 @@ class WalletConnectInteractor( val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics) handler.onListOfSessionsUpdated(filteredSessions) } - .flowOn(dispatcher.io) + .flowOn(dispatchers.io) .collect() } @@ -197,10 +260,18 @@ class WalletConnectInteractor( ) else -> { currentRequest = sessionRequest - val data = prepareRequestData(sessionRequest) - if (data != null) { - handler.onSessionRequest(data) + + val data = prepareRequestData(sessionRequest).getOrElse { e -> + val wrappedError = e as? WalletConnectError ?: WalletConnectError.UnknownError( + message = e.localizedMessage ?: "Unknown error", + ) + + walletConnectRepository.rejectRequest(requestData, wrappedError) + handler.onSessionRejected(wrappedError) + return } + + handler.onSessionRequest(data) } } } @@ -258,7 +329,41 @@ class WalletConnectInteractor( return uri.lowercase().startsWith(WC_SCHEME) } - private suspend fun prepareRequestData(sessionRequest: WalletConnectEvents.SessionRequest): WcPreparedRequest? { + private fun getCardId(userWallet: UserWallet): String? { + return if (userWallet.scanResponse.card.backupStatus?.isActive != true) { + userWallet.cardId + } else { // if wallet has backup, any card from wallet can be used to sign + null + } + } + + private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List): List { + val walletManagers = networks.mapNotNull { + val blockchain = Blockchain.fromId(it.id.value) + walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + blockchain = blockchain, + derivationPath = it.derivationPath.value, + ) + } + return walletManagers.mapNotNull { + val wallet = it.wallet + val chainId = blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } + } + } + + private suspend fun prepareRequestData( + sessionRequest: WalletConnectEvents.SessionRequest, + ): Result { return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt index 33d62fca22..23e7f0bc82 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt @@ -27,5 +27,5 @@ interface WalletConnectRepository { fun rejectRequest(requestData: RequestData, error: WalletConnectError) - fun cancelRequest(topic: String, id: Long) + fun cancelRequest(topic: String, id: Long, message: String = "") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt index 12085cad12..e02d7cc679 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt @@ -4,6 +4,7 @@ import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType @@ -17,15 +18,18 @@ internal class WcSessionRequestConverter( suspend fun prepareRequest( sessionRequest: WalletConnectEvents.SessionRequest, userWalletId: String, - ): WcPreparedRequest? { - val networkId = blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId ?: "") ?: return null + ): Result = runCatching { + val networkId = requireNotNull(blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId.orEmpty())) { + "Failed to get network ID for chain ID: ${sessionRequest.chainId}" + } val derivationPath = getDerivationPath( sessionsRepository = sessionsRepository, sessionRequest = sessionRequest, userWalletId = userWalletId, walletAddress = getWalletAddress(sessionRequest.request), ) - return when (val request = sessionRequest.request) { + + when (val request = sessionRequest.request) { is WcRequest.EthSendTransaction -> { val data = sdkHelper.prepareTransactionData( EthTransactionData( @@ -38,7 +42,8 @@ internal class WcSessionRequestConverter( metaName = sessionRequest.metaName, metaUrl = sessionRequest.metaUrl, ), - ) ?: return null + ) + WcPreparedRequest.EthTransaction( preparedRequestData = data, topic = sessionRequest.topic, @@ -58,7 +63,8 @@ internal class WcSessionRequestConverter( metaName = sessionRequest.metaName, metaUrl = sessionRequest.metaUrl, ), - ) ?: return null + ) + WcPreparedRequest.EthTransaction( preparedRequestData = data, topic = sessionRequest.topic, @@ -122,7 +128,7 @@ internal class WcSessionRequestConverter( derivationPath = derivationPath, ) } - else -> null + else -> throw WalletConnectError.UnsupportedMethod } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt index 7d6504d923..40d82cdb51 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt @@ -20,8 +20,12 @@ sealed class WalletConnectError(val error: String) : Exception() { override val message: String?, ) : WalletConnectError("ExternalApprovalError") - object WrongUserWallet : WalletConnectError("WrongUserWallet") - object UnsupportedMethod : WalletConnectError("UnsupportedMethod") - object SigningError : WalletConnectError("SigningError") - object ValidationError : WalletConnectError("ValidationError") + data class UnknownError( + override val message: String, + ) : WalletConnectError(message) + + data object WrongUserWallet : WalletConnectError("WrongUserWallet") + data object UnsupportedMethod : WalletConnectError("UnsupportedMethod") + data object SigningError : WalletConnectError("SigningError") + data object ValidationError : WalletConnectError("ValidationError") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt index 96f2eea824..e9b45a2623 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.customtoken.impl.data import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index 2de3e037af..de73abac25 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.customtoken.impl.domain import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index 3900824553..da295c57c5 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel import dagger.hilt.android.AndroidEntryPoint @@ -23,7 +23,7 @@ import javax.inject.Inject internal class AddCustomTokenFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Composable override fun ScreenContent(modifier: Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt index 55afd696af..afff4bf8e5 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm @@ -70,7 +71,7 @@ internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, mod @Preview @Composable private fun Preview_AddCustomTokenContent() { - TangemTheme { + TangemThemePreview { AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt index 3824462da3..d2e3729321 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt @@ -1,11 +1,12 @@ package com.tangem.tap.features.customtoken.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder /** @@ -24,21 +25,12 @@ internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifi } @Preview(showSystemUi = true) +@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AddCustomTokenScreen_Light( +private fun Preview_AddCustomTokenScreen( @PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder, ) { - TangemTheme(isDark = false) { - AddCustomTokenScreen(stateHolder) - } -} - -@Preview(showSystemUi = true) -@Composable -private fun Preview_AddCustomTokenScreen_Dark( - @PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder, -) { - TangemTheme(isDark = true) { + TangemThemePreview { AddCustomTokenScreen(stateHolder) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt index 48ffb09110..3bb17cddb9 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock @@ -242,7 +243,7 @@ private fun TestBlock( @Preview @Composable private fun Preview_AddCustomTokenTestContent() { - TangemTheme { + TangemThemePreview { AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt index a55ee2f29e..3cc1d4847a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton import com.tangem.wallet.R @@ -42,7 +43,7 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, m private fun Preview_AddCustomTokenFloatingButton( @PreviewParameter(AddCustomTokenFloatingButtonProvider::class) model: AddCustomTokenFloatingButton, ) { - TangemTheme { + TangemThemePreview { AddCustomTokenFloatingButton(model) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt index dd1293ba65..58da365d81 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.common.compose.TangemTextFieldsDefault import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField @@ -195,7 +196,7 @@ private fun SelectorField(model: AddCustomTokenSelectorField) { @Preview @Composable private fun Preview_AddCustomTokenForm(@PreviewParameter(AddCustomTokenFormProvider::class) model: AddCustomTokenForm) { - TangemTheme { + TangemThemePreview { AddCustomTokenForm(model) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt index 81d0f52912..030f130a1a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -54,7 +55,7 @@ internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () - @Preview @Composable internal fun Preview_AddCustomTokenToolbar() { - TangemTheme { + TangemThemePreview { AddCustomTokenToolbar(title = TextReference.Res(R.string.add_custom_token_title), onBackButtonClick = {}) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt index a00fc2d4fb..a323a8a982 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.customtoken.impl.presentation.ui.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -15,6 +16,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData @@ -71,17 +73,10 @@ private fun AddCustomTokenWarning(warning: AddCustomTokenWarning) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AddCustomTokenWarnings_Light() { - TangemTheme { - AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings()) - } -} - -@Preview -@Composable -private fun Preview_AddCustomTokenWarnings_Dark() { - TangemTheme(isDark = true) { +private fun Preview_AddCustomTokenWarnings() { + TangemThemePreview { AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt index 5db91d6354..6bb0c880af 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt @@ -23,12 +23,12 @@ object ContractAddressValidator { private fun validateAddress(blockchain: Blockchain, address: String): Boolean { return when (blockchain) { - Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet -> { - SuccessAddressValidator.validate(address) - } - else -> { - blockchain.validateAddress(address) - } + Blockchain.Unknown, + Blockchain.Binance, + Blockchain.BinanceTestnet, + Blockchain.Cardano, + -> SuccessAddressValidator.validate(address) + else -> blockchain.validateAddress(address) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 3dd2804b29..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..372889fe91 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 @@ -16,23 +15,24 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.userwallets.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.tap.* +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation -import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R @@ -83,6 +83,8 @@ class DetailsMiddleware { class EraseWalletMiddleware { @Suppress("CyclomaticComplexMethod") fun handle(action: DetailsAction.ResetToFactory) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + when (action) { is DetailsAction.ResetToFactory.Start -> { val card = store.state.detailsState.cardSettingsState?.card ?: return @@ -329,8 +331,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 +372,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 +389,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 +399,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 +416,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 +494,8 @@ class DetailsMiddleware { val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() // Update access code policy for access code saving when a card was scanned - cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, - ) + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = shouldSaveAccessCodes) store.inject(DaggerGraphState::scanCardProcessor).scan( analyticsSource = CoreAnalyticsParam.ScreensSources.Settings, @@ -588,35 +507,70 @@ class DetailsMiddleware { store.dispatchOnMain(NavigationAction.PopBackTo()) }, onSuccess = { scanResponse -> - saveUserWalletAndPopBackToWalletScreen(scanResponse) + createUserWallet(scanResponse) + .doOnSuccess { + saveUserWalletAndPopBackToWalletScreen( + userWallet = it, + prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode, + ) + } .doOnFailure { error -> - // Rollback policy if card saving was failed - cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) - Timber.e(error, "Unable to save user wallet") - - store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + Timber.e(error, "Unable to create user wallet") + handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode) } }, onFailure = { error -> - // Rollback policy if card scanning was failed - cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) Timber.e(error, "Unable to scan card") - store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode) }, ) } - private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult { - val userWallet = UserWalletBuilder(scanResponse).build() - ?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + private suspend fun createUserWallet(scanResponse: ScanResponse): CompletionResult { + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build() - return userWalletsListManager.save(userWallet) + return if (userWallet != null) { + CompletionResult.Success(userWallet) + } else { + CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + } + } + + private suspend fun saveUserWalletAndPopBackToWalletScreen( + userWallet: UserWallet, + prevUseBiometricsForAccessCode: Boolean, + ) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + + userWalletsListManager.save(userWallet) .doOnSuccess { store.onUserWalletSelected(userWallet) store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) } + .doOnFailure { error -> + if (error is UserWalletsListError.WalletAlreadySaved) { + userWalletsListManager.select(userWallet.walletId) + store.onUserWalletSelected(userWallet) + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + } else { + Timber.e(error, "Unable to create user wallet") + handleError(error, prevUseBiometricsForAccessCode) + } + } + } + + private suspend fun handleError(error: TangemError, prevUseBiometricsForAccessCode: Boolean) { + val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository) + + // Rollback policy if card saving was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) } private fun TangemError.toTextReference(): TextReference? { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 5b3f2fafd3..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..7b34183f53 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -1,44 +1,27 @@ package com.tangem.tap.features.details.redux.walletconnect import androidx.core.os.bundleOf -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.common.extensions.guard +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.walletconnect.WalletConnectActions -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect.BnbHelper -import com.tangem.tap.domain.walletconnect.WalletConnectManager -import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils -import com.tangem.tap.domain.walletconnect.extensions.toWcEthTransaction import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.Account -import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager -import kotlinx.coroutines.Dispatchers +import com.tangem.wallet.R import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -46,7 +29,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 +48,18 @@ class WalletConnectMiddleware { if (DemoHelper.tryHandle(state, action)) return when (action) { - is WalletConnectActions.New.Initialize -> { - val userWallet = action.userWallet - val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) { - userWallet.cardId - } else { // if wallet has backup, any card from wallet can be used to sign - null - } - scope.launch { - val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch - wcInteractor.startListening( - userWalletId = userWallet.walletId.stringValue, - cardId = cardId, - ) - } - } - is WalletConnectActions.New.SetupUserChains -> { - scope.launch { - val userWallet = action.userWallet - val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch - wcInteractor.setUserChains(getAccountsForWc(wcInteractor, userWallet)) - } - } - is WalletConnectAction.ResetState -> walletConnectManager = WalletConnectManager() - is WalletConnectAction.RestoreSessions -> { - walletConnectManager.restoreSessions(action.scanResponse) - } is WalletConnectAction.HandleDeepLink -> { if (!action.wcUri.isNullOrBlank()) { store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri)) } } + is WalletConnectAction.DisconnectSession -> { + walletConnectInteractor.disconnectSession(action.topic) + } is WalletConnectAction.StartWalletConnect -> { val uri = action.copiedUri if (uri != null && isWalletConnectUri(uri)) { + // TODO check store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { store.dispatchOnMain( @@ -112,27 +72,6 @@ class WalletConnectMiddleware { ) } } - is WalletConnectAction.SelectNetwork -> { - store.dispatch( - GlobalAction.ShowDialog( - WalletConnectDialog.ChooseNetwork( - session = action.session, - networks = action.networks, - ), - ), - ) - } - is WalletConnectAction.ChooseNetwork -> { - val data = state()?.walletConnectState?.newSessionData ?: return - scope.launch { - prepareWalletManager( - scanResponse = data.scanResponse, - blockchain = action.blockchain, - session = data.session, - walletConnectManager = walletConnectManager, - ) - } - } is WalletConnectAction.ShowClipboardOrScanQrDialog -> { store.dispatchOnMain( GlobalAction.ShowDialog( @@ -142,176 +81,15 @@ class WalletConnectMiddleware { ), ) } - is WalletConnectAction.OpeningSessionTimeout -> { - Timber.e("OpeningSessionTimeout for topic ${action.session.topic}") - // do not show dialog for now, it shows always to user if cannot establish connection - // store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout)) - } - is WalletConnectAction.FailureEstablishingSession -> { - Timber.e("FailureEstablishingSession for topic ${action.session?.topic}") - // disable alerts in release to avoid annoying users - // if (action.error != null) { - // store.dispatch( - // GlobalAction.ShowDialog( - // AppDialog.SimpleOkDialogRes( - // headerId = R.string.common_warning, - // messageId = action.error.messageResource, - // ), - // ), - // ) - // } - if (action.session != null) { - walletConnectManager.disconnect(action.session) - } - } - is WalletConnectAction.UnsupportedCard -> { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard)) - } is WalletConnectAction.OpenSession -> { val index = action.wcUri.indexOf("@") when (action.wcUri[index + 1]) { - '1' -> { - walletConnectManager.connect(wcUri = action.wcUri) - } '2' -> walletConnectRepository.pair(uri = action.wcUri) } } - is WalletConnectAction.RefuseOpeningSession -> { - Timber.e("RefuseOpeningSession") - - // do not show for now to avoid anoying users with alert - // store.dispatch( - // GlobalAction.ShowDialog( - // WalletConnectDialog.OpeningSessionRejected, - // ), - // ) - } - is WalletConnectAction.ScanCard -> { - val scanResponse = userWalletsListManager.selectedUserWalletSync.guard { - Timber.w("Unable to get selected user wallet for WC session") - return - } - - scope.launch(Dispatchers.Main) { - scanCard(scanResponse, action.session, action.chainId) - } - } - is WalletConnectAction.ApproveSession -> { - walletConnectManager.approve(action.session) - } - is WalletConnectAction.DisconnectSession -> { - if (action.session != null) { - walletConnectManager.disconnect(action.session) - } else { - walletConnectInteractor.disconnectSession(action.topic) - } - } - is WalletConnectAction.HandleTransactionRequest -> { - walletConnectManager.handleTransactionRequest( - transaction = action.transaction.toWcEthTransaction(), - session = action.session, - id = action.id, - type = action.type, - ) - } - is WalletConnectAction.HandlePersonalSignRequest -> { - walletConnectManager.handlePersonalSignRequest( - message = action.message, - session = action.session, - id = action.id, - ) - } is WalletConnectAction.RejectRequest -> { - walletConnectManager.rejectRequest(action.topic, action.id) walletConnectInteractor.cancelRequest(action.topic, action.id) } - is WalletConnectAction.SendTransaction -> { - walletConnectManager.completeTransaction(action.topic) - } - is WalletConnectAction.SignMessage -> { - walletConnectManager.sendSignedMessage(action.topic) - } - is WalletConnectAction.BinanceTransaction.Trade -> { - val messageData = BnbHelper.createMessageData(action.order) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.BnbTransactionDialog( - WcPreparedRequest.BnbTransaction( - preparedRequestData = BnbData( - data = messageData, - topic = action.sessionData.session.topic, - requestId = action.id, - dAppName = action.sessionData.peerMeta.name, - ), - topic = action.sessionData.session.topic, - requestId = action.id, - derivationPath = action.sessionData.wallet.derivationPath?.rawPath, - ), - ), - ), - ) - } - is WalletConnectAction.BinanceTransaction.Transfer -> { - val messageData = BnbHelper.createMessageData(action.order) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.BnbTransactionDialog( - WcPreparedRequest.BnbTransaction( - preparedRequestData = BnbData( - data = messageData, - topic = action.sessionData.session.topic, - requestId = action.id, - dAppName = action.sessionData.peerMeta.name, - ), - topic = action.sessionData.session.topic, - requestId = action.id, - derivationPath = action.sessionData.wallet.derivationPath?.rawPath, - ), - ), - ), - ) - } - is WalletConnectAction.BinanceTransaction.Sign -> { - walletConnectManager.signBnb( - id = action.id, - data = action.data, - topic = action.topic, - ) - } - is WalletConnectAction.SwitchBlockchain -> { - if (action.session.wallet.derivationStyle == DerivationStyle.LEGACY) { - Timber.d("Cannot switch chains on AC01/AC02 wallets") - return - } - val blockchain = action.blockchain.guard { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) - return - } - scope.launch { - val walletManager = getWalletManager( - wallet = action.session.wallet, - blockchain = blockchain, - ).guard { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)), - ), - ) - return@launch - } - val updatedWallet = action.session.wallet.copy( - walletPublicKey = walletManager.wallet.publicKey.seedKey, - derivedPublicKey = walletManager.wallet.publicKey.derivedKey, - derivationPath = walletManager.wallet.publicKey.derivationPath, - blockchain = action.blockchain, - ) - val updatedSession = action.session.copy(wallet = updatedWallet) - store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession)) - } - } - is WalletConnectAction.UpdateBlockchain -> { - walletConnectManager.updateBlockchain(action.updatedSession) - } is WalletConnectAction.ApproveProposal -> { scope.launch { val accounts = getWalletManagers() @@ -359,6 +137,17 @@ class WalletConnectMiddleware { ), ) } + is WalletConnectError.UnknownError -> { + store.dispatchOnMain( + GlobalAction.ShowDialog( + AppDialog.SimpleOkDialogRes( + headerId = R.string.wallet_connect_title, + messageId = R.string.wallet_connect_error_with_framework_message, + args = listOf(action.error.message), + ), + ), + ) + } is WalletConnectError.ExternalApprovalError -> { Timber.e(action.error, "ExternalApprovalError ${action.error.message}") // do not show dialog on this event @@ -397,140 +186,13 @@ class WalletConnectMiddleware { private suspend fun getWalletManagers(): List { val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() return walletManagerFacade.getStoredWalletManagers(userWallet.walletId) } - private suspend fun scanCard(userWallet: UserWallet, session: WalletConnectSession, chainId: Int?) { - val blockchain = WalletConnectNetworkUtils.parseBlockchain( - chainId = chainId, - peer = session.peerMeta, - ).guard { - store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) - return - } - - handleScanResponse(userWallet, session, blockchain) - } - - private suspend fun getAvailableEvmBlockchains(userWalletId: UserWalletId): List { - val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository) - - return currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - .asSequence() - .filterIsInstance() - .filterNot { it.isCustom } - .mapNotNull { Blockchain.fromNetworkId(it.network.id.value) } - .filter { it.isEvm() } - .toList() - } - - private suspend fun prepareWalletManager( - scanResponse: ScanResponse, - blockchain: Blockchain, - session: WalletConnectSession, - walletConnectManager: WalletConnectManager, - ) { - val walletManager = getWalletManager(session.wallet, blockchain).guard { - store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session)) - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)), - ), - ) - return - } - val wallet = walletManager.wallet - val derivedKey = - if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) { - null - } else { - walletManager.wallet.publicKey.blockchainKey - } - val walletForSession = WalletForSession( - walletPublicKey = wallet.publicKey.seedKey, - derivedPublicKey = derivedKey, - derivationPath = wallet.publicKey.derivationPath, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - blockchain = wallet.blockchain, - ) - - withMainContext { - val updatedSession = session.copy(wallet = walletForSession) - walletConnectManager.updateSession(updatedSession) - - store.dispatch(WalletConnectAction.ApproveSession(session.session)) - } - } - - private suspend fun handleScanResponse( - userWallet: UserWallet, - session: WalletConnectSession, - blockchain: Blockchain, - ) { - val scanResponse = userWallet.scanResponse - - if (!scanResponse.cardTypesResolver.isMultiwalletAllowed()) { - store.dispatchOnMain(WalletConnectAction.UnsupportedCard) - return - } - val updatedSession = session.copy(wallet = session.wallet.copy(blockchain = blockchain)) - store.dispatch( - WalletConnectAction.SetNewSessionData( - NewWcSessionData(updatedSession, scanResponse, blockchain), - ), - ) - val blockchains = if (blockchain.isEvm()) { - getAvailableEvmBlockchains(userWallet.walletId) - } else { - emptyList() - } - store.dispatch( - GlobalAction.ShowDialog( - WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains), - ), - ) - } - - private suspend fun getWalletManager(wallet: WalletForSession, blockchain: Blockchain): WalletManager? { - val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) { - Blockchain.EthereumTestnet - } else { - blockchain - } - val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null - val derivation = blockchainToMake.derivationPath( - style = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), - )?.rawPath - - val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) - - return walletManagerFacade.getOrCreateWalletManager( - userWalletId = userWallet.walletId, - blockchain = blockchainToMake, - derivationPath = derivation, - ) - } - private fun isWalletConnectUri(uri: String): Boolean { - return WalletConnectManager.isCorrectWcUri(uri) || walletConnectInteractor.isWalletConnectUri(uri) - } - - private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor, userWallet: UserWallet): List { - val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade) - return walletManagerFacade.getStoredWalletManagers(userWallet.walletId).mapNotNull { - val wallet = it.wallet - val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), - ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } - } + return walletConnectInteractor.isWalletConnectUri(uri) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt index 6840f26c63..20217386f2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -7,36 +7,9 @@ object WalletConnectReducer { if (action !is WalletConnectAction) return state return when (action) { - is WalletConnectAction.ResetState -> return WalletConnectState() - is WalletConnectAction.ApproveSession.Success -> { - state.copy( - loading = false, - sessions = state.sessions + action.session, - ) - } is WalletConnectAction.OpenSession -> { state.copy(loading = true) } - is WalletConnectAction.SetNewSessionData -> { - state.copy(newSessionData = action.newSession) - } - is WalletConnectAction.SetSessionsRestored -> state.copy( - sessions = action.sessions, - ) - is WalletConnectAction.RemoveSession -> { - val sessions = - state.sessions.filterNot { it.session.toUri() == action.session.toUri() } - state.copy(sessions = sessions) - } - is WalletConnectAction.UnsupportedCard, - is WalletConnectAction.RefuseOpeningSession, - is WalletConnectAction.OpeningSessionTimeout, - is WalletConnectAction.FailureEstablishingSession, - -> state.copy(loading = false) - is WalletConnectAction.UpdateBlockchain -> state.copy( - sessions = state.sessions - .filterNot { it.peerId == action.updatedSession.peerId } + action.updatedSession, - ) is WalletConnectAction.ApproveProposal -> state.copy(loading = true) is WalletConnectAction.RejectProposal, is WalletConnectAction.SessionEstablished, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 68cfd4255b..750f4b04c5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -14,12 +14,9 @@ import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData -import com.trustwallet.walletconnect.models.WCPeerMeta -import com.trustwallet.walletconnect.models.session.WCSession data class WalletConnectState( val loading: Boolean = false, - val sessions: List = listOf(), val wc2Sessions: List = listOf(), val newSessionData: NewWcSessionData? = null, ) @@ -34,8 +31,6 @@ data class WalletConnectSession( val peerId: String, val remotePeerId: String?, val wallet: WalletForSession, - val session: WCSession, - val peerMeta: WCPeerMeta, ) { fun getAddress(): String? { val key = wallet.derivedPublicKey ?: wallet.walletPublicKey ?: return null diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt index b9273ef99b..d48050ec85 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -16,7 +16,7 @@ import javax.inject.Inject internal class AppCurrencySelectorFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: AppCurrencySelectorViewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt index a38cac5c0a..b29b503a39 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appcurrency +import android.content.res.Configuration import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -25,6 +26,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency import com.tangem.wallet.R @@ -302,21 +304,12 @@ private val RadioButtonColors: RadioButtonColors // region Preview @Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AppCurrencySelectorScreenPreview_Light( +private fun AppCurrencySelectorScreenPreview( @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, ) { - TangemTheme(isDark = false) { - AppCurrencySelectorScreen(param) - } -} - -@Preview(showBackground = true, widthDp = 360, heightDp = 720) -@Composable -private fun AppCurrencySelectorScreenPreview_Dark( - @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { AppCurrencySelectorScreen(param) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 61e02daeda..2efc912cca 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -7,8 +7,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store @@ -19,7 +19,7 @@ import javax.inject.Inject internal class AppSettingsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var appCurrencyRepository: AppCurrencyRepository diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 7c99ed8b24..c60f34ecce 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings +import android.content.res.Configuration import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -10,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -70,21 +72,12 @@ private fun AppSettings(state: AppSettingsScreenState.Content) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AppSettingsScreenPreview_Light( +private fun AppSettingsScreenPreview( @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, ) { - TangemTheme { - AppSettingsScreen(state = state, onBackClick = {}) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreenPreview_Dark( - @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { AppSettingsScreen(state = state, onBackClick = {}) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index 1dce42e622..b7b5b15da2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -8,7 +9,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @@ -34,17 +35,10 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { - TangemTheme { - SettingsAlertDialog(dialog = dialog) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { - TangemTheme(isDark = true) { +private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { + TangemThemePreview { SettingsAlertDialog(dialog = dialog) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt index 946c62d2f2..17dccc7a63 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -14,6 +15,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -52,17 +54,10 @@ internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { - TangemTheme { - SettingsButtonItem(item = item) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { - TangemTheme(isDark = true) { +private fun ButtonItemPreview(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemThemePreview { SettingsButtonItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt index 1560b14a43..1e43c8f328 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon @@ -16,6 +17,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW16 import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -59,17 +61,10 @@ internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) { - TangemTheme { - SettingsCardItem(item = item) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) { - TangemTheme(isDark = true) { +private fun CardItemPreview(@PreviewParameter(CardItemProvider::class) item: Item.Card) { + TangemThemePreview { SettingsCardItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt index 8c1a93e307..41b568b7ed 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -8,7 +9,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.components.SelectorDialog import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @@ -31,17 +32,10 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { - TangemTheme(isDark = false) { - SettingsSelectorDialog(param) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { - TangemTheme(isDark = true) { +private fun SettingsSelectorDialogPreview(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemThemePreview { SettingsSelectorDialog(param) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt index dfbc486b5c..a8476e3bd2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW32 import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @@ -71,17 +73,10 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { - TangemTheme { - SettingsSwitchItem(item = item) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { - TangemTheme(isDark = true) { +private fun SwitchItemPreview(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemThemePreview { SettingsSwitchItem(item = item) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 82431ec211..c81e2d819e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -5,8 +5,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.viewModels import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -16,7 +16,7 @@ import javax.inject.Inject internal class CardSettingsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: CardSettingsViewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index b528123bb7..b0e2785b32 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.cardsettings +import android.content.res.Configuration import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -12,6 +13,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold @@ -182,17 +184,10 @@ private fun CardSettingsScreenStateSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun CardSettingsScreenStatePreview_Light() { - TangemTheme { - CardSettingsScreenStateSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun CardSettingsScreenStatePreview_Dark() { - TangemTheme(isDark = true) { +private fun CardSettingsScreenStatePreview() { + TangemThemePreview { CardSettingsScreenStateSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt index 3fa69eebb7..7a365126b6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -22,7 +22,7 @@ class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var walletsRepository: WalletsRepository @Inject - lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles - - @Inject - lateinit var getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase - - @Inject - lateinit var emailSender: EmailSender + lateinit var userWalletsListManager: UserWalletsListManager private lateinit var detailsViewModel: DetailsViewModel @@ -43,9 +35,7 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber, private val walletsRepository: WalletsRepository, - private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, - private val getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase, - private val emailSender: EmailSender, + private val userWalletsListManager: UserWalletsListManager, ) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) @@ -148,21 +141,7 @@ internal class DetailsViewModel( private fun sendFeedback() { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings)) - if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { - mainScope.launch { - val email = getSupportFeedbackEmailUseCase() - emailSender.send( - email = EmailSender.Email( - address = email.address, - subject = email.subject, - message = email.message, - attachment = email.file, - ), - ) - } - } else { - store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) - } + store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) } private fun navigateToAppSettings() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index 5414ff05c6..1522c07ed3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -17,7 +17,7 @@ import javax.inject.Inject internal class ResetCardFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel = ResetCardViewModel(store) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 35b4c9bae8..b0ccd93897 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.ui.resetcard +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -12,6 +13,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -228,17 +230,10 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ResetCardScreenPreview_Light() { - TangemTheme { - ResetCardScreenSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun ResetCardScreenPreview_Dark() { - TangemTheme(isDark = true) { +private fun ResetCardScreenPreview() { + TangemThemePreview { ResetCardScreenSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt new file mode 100644 index 0000000000..2de1e70012 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.details.ui.resetcard.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.tap.features.details.ui.resetcard.featuretoggles.DefaultResetCardFeatureToggles +import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ResetCardModule { + + @Provides + @Singleton + fun provideResetCardFeatureToggles(featureTogglesManager: FeatureTogglesManager): ResetCardFeatureToggles { + return DefaultResetCardFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt new file mode 100644 index 0000000000..a5b76a410d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.details.ui.resetcard.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultResetCardFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : ResetCardFeatureToggles { + + override val isFullResetEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "FULL_RESET_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt new file mode 100644 index 0000000000..d290d4d1d9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.details.ui.resetcard.featuretoggles + +interface ResetCardFeatureToggles { + + val isFullResetEnabled: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index 2767f56545..c44b9ca3fa 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -17,7 +17,7 @@ import javax.inject.Inject internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel = SecurityModeViewModel(store) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt index 1733d8d946..19dd0771bc 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -8,10 +8,9 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.WalletConnect -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -22,7 +21,7 @@ import javax.inject.Inject internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: WalletConnectViewModel by viewModels() @@ -42,13 +41,6 @@ internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions + val sessions = state.wc2Sessions return WalletConnectScreenState( sessions.toImmutableList(), isLoading = state.loading, - onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions, state.wc2Sessions) }, + onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) }, onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) }, ) } - private fun onRemoveSession( - sessionUri: String, - sessions: List, - wc2sessions: List, - ) { - sessions - .firstOrNull { it.session.toUri() == sessionUri } - ?.let { baseSession -> - store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session.topic, baseSession.session)) - return - } + private fun onRemoveSession(sessionUri: String, wc2sessions: List) { wc2sessions.firstOrNull { it.sessionId == sessionUri }?.let { - store.dispatch(WalletConnectAction.DisconnectSession(sessionUri, null)) + store.dispatch(WalletConnectAction.DisconnectSession(sessionUri)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt deleted file mode 100644 index d49a684033..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.store -import com.tangem.wallet.R - -object ApproveWcSessionDialog { - fun create(session: WalletConnectSession, networks: List, context: Context): AlertDialog { - val sessionBlockchain = requireNotNull(session.wallet.blockchain) { "session network is null" } - val message = context.getString( - R.string.wallet_connect_request_session_start, - session.peerMeta.name, - sessionBlockchain.fullName, - session.peerMeta.url, - ) - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.wallet_connect_title)) - setMessage(message) - setPositiveButton(context.getText(R.string.common_start)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.ChooseNetwork(sessionBlockchain)) - } - if (networks.size > 1) { - setNeutralButton(context.getText(R.string.wallet_connect_select_network)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.SelectNetwork(session = session, networks = networks)) - } - } - setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) - } - setOnCancelListener { - store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt index cddaab442a..aee88834e4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt @@ -42,13 +42,6 @@ object BnbTransactionDialog { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(fullMessage) setPositiveButton(positiveButtonTitle) { _, _ -> - store.dispatch( - WalletConnectAction.BinanceTransaction.Sign( - id = preparedData.requestId, - data = data.data, - topic = preparedData.topic, - ), - ) store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt deleted file mode 100644 index b99df2b02e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession -import com.tangem.tap.store -import com.tangem.wallet.R - -object ChooseNetworkDialog { - fun create(session: WalletConnectSession, networks: List, context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog) - .setTitle(context.getString(R.string.wallet_connect_select_network)) - .setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> - store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) - } - .setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - .setSingleChoiceItems(networks.map { it.fullName }.toTypedArray(), 0) { _, which -> - networks.getOrNull(which)?.let { selectedBlockchain -> - store.dispatch( - WalletConnectAction.ChooseNetwork( - blockchain = selectedBlockchain, - ), - ) - store.dispatch(GlobalAction.HideDialog) - } - } - .create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt index 4230afdeaf..4403b6d4a6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt @@ -17,7 +17,6 @@ object PersonalSignDialog { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> - store.dispatch(WalletConnectAction.SignMessage(data.topic)) store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt index 608eebc130..3ec2d8bfda 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt @@ -32,11 +32,9 @@ object TransactionDialog { setMessage(message) setPositiveButton(positiveButtonTitle) { _, _ -> if (data.isEnoughFundsToSend) { - store.dispatch(WalletConnectAction.SendTransaction(data.topic)) store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData)) } else { store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id)) - store.dispatch(WalletConnectAction.NotEnoughFunds) } } setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 90bb65ce39..8e081474fb 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -12,9 +12,9 @@ import androidx.lifecycle.lifecycleScope import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.redux.AppState @@ -30,7 +30,7 @@ import javax.inject.Inject class HomeFragment : ComposeFragment(), StoreSubscriber { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private var homeState: MutableState = mutableStateOf(store.state.homeState) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index bd3f2a5dde..1b0c69fb9c 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -21,7 +21,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.compose.resources.C +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags import com.tangem.tap.features.home.compose.content.* import com.tangem.tap.features.home.compose.views.HomeButtons import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton @@ -57,7 +58,7 @@ fun StoriesScreen( } StoriesScreenContent( - modifier = Modifier.fillMaxSize().testTag(C.Tag.STORIES_SCREEN), + modifier = Modifier.fillMaxSize().testTag(TestTags.STORIES_SCREEN), config = StoriesScreenContentConfig( storiesSize = state.stories.lastIndex, currentStoryIndex = currentStoryIndex, @@ -213,7 +214,7 @@ private data class StoriesScreenContentConfig( private fun StoriesScreenContentPreview( @PreviewParameter(StoriesScreenContentConfigProvider::class) config: StoriesScreenContentConfig, ) { - TangemTheme { + TangemThemePreview { StoriesScreenContent(config = config) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index 75d308e2b7..be6823d684 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -16,7 +16,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.compose.resources.C +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags import com.tangem.wallet.R @Composable @@ -33,7 +34,7 @@ internal fun HomeButtons( ScanCardButton( modifier = Modifier .weight(weight = 1f) - .testTag(C.Tag.STORIES_SCREEN_SCAN_BUTTON), + .testTag(TestTags.STORIES_SCREEN_SCAN_BUTTON), showProgress = btnScanStateInProgress, onClick = onScanButtonClick, ) @@ -41,7 +42,7 @@ internal fun HomeButtons( OrderCardButton( modifier = Modifier .weight(weight = 1f) - .testTag(C.Tag.STORIES_SCREEN_ORDER_BUTTON), + .testTag(TestTags.STORIES_SCREEN_ORDER_BUTTON), onClick = onShopButtonClick, ) } @@ -73,7 +74,7 @@ private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) @Preview(showBackground = true, widthDp = 360) @Composable private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::class) state: HomeButtonsState) { - TangemTheme { + TangemThemePreview { Box( modifier = Modifier.background(Color.Black), ) { diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt index 4e91ce98bc..e3e237954e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @Composable @@ -29,7 +30,7 @@ internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Mo @Preview(showBackground = true, widthDp = 360) @Composable private fun SearchCurrenciesButtonPreview() { - TangemTheme { + TangemThemePreview { Box( modifier = Modifier .background(color = Color.Black) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 40110a5a22..17e8d7dd0a 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -11,7 +11,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop @@ -19,11 +19,9 @@ import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL -import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -77,8 +75,10 @@ private fun handleHomeAction(action: Action) { } private suspend fun readCard() { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + isBiometricsRequestPolicy = shouldSaveAccessCodes, ) store.inject(DaggerGraphState::scanCardProcessor).scan( @@ -103,11 +103,13 @@ private suspend fun readCard() { } private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { - val userWallet = UserWalletBuilder(scanResponse).build().guard { + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build().guard { Timber.e("User wallet not created") return@launch } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") @@ -127,6 +129,8 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { ) if (currency != null) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + Analytics.send( event = Basic.SignedIn( currency = currency, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/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 d61c7ffebf..53a3565731 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -2,6 +2,9 @@ package com.tangem.tap.features.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController @@ -10,13 +13,20 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase +import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.main.model.MainScreenState +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -27,7 +37,12 @@ internal class MainViewModel @Inject constructor( private val reduxNavController: ReduxNavController, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, + private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, + private val blockchainSDKFactory: BlockchainSDKFactory, + private val userWalletsListManager: UserWalletsListManager, + private val walletManagersFacade: WalletManagersFacade, private val sendFeatureToggles: SendFeatureToggles, + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -41,7 +56,14 @@ internal class MainViewModel @Inject constructor( val state: StateFlow = stateHolder.stateFlow + var isSplashScreenShown: Boolean = true + private set + init { + loadApplicationResources() + + viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } + updateAppCurrencies() updateSendFeatureToggle() observeFlips() @@ -53,6 +75,40 @@ internal class MainViewModel @Inject constructor( } } + /** Loading the resources needed to run the application */ + private fun loadApplicationResources() { + viewModelScope.launch(dispatchers.main) { + blockchainSDKFactory.init() + prepareSelectedWalletFeedback() + + isSplashScreenShown = false + } + } + + private fun prepareSelectedWalletFeedback() { + userWalletsListManager.selectedUserWallet + .distinctUntilChanged() + .onEach { userWallet -> + Analytics.setContext(userWallet.scanResponse) + Analytics.send(Basic.WalletOpened()) + + if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) { + store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder -> + infoHolder.setCardInfo(userWallet.scanResponse) + + walletManagersFacade + .getAll(userWallet.walletId) + .distinctUntilChanged() + .onEach(infoHolder::setWalletsInfo) + .catch { Timber.e(it) } + .launchIn(viewModelScope) + } + } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + private fun updateAppCurrencies() { viewModelScope.launch(dispatchers.main) { fetchAppCurrenciesUseCase.invoke() diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt index ec7cb8d416..ef72720cad 100644 --- a/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/main/ui/ModalNotificationBottomSheetFragment.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.activityViewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.ModalNotification import com.tangem.tap.features.main.ui.components.ModalNotificationContent @@ -22,7 +22,7 @@ import javax.inject.Inject internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel: MainViewModel by activityViewModels() diff --git a/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt b/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt index 587b734dc0..72c36caf54 100644 --- a/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/main/ui/components/ModalNotificationScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.main.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Icon @@ -17,6 +18,7 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.main.model.ActionConfig import com.tangem.tap.features.main.model.ModalNotification @@ -82,27 +84,12 @@ internal fun ModalNotificationContent(notification: ModalNotification, modifier: // region Preview @Preview(widthDp = 360, heightDp = 404) +@Preview(widthDp = 360, heightDp = 404, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ModalNotificationContentPreview_Light( +private fun ModalNotificationContentPreview( @PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification, ) { - TangemTheme(isDark = false) { - ModalNotificationContent( - notification = param, - modifier = Modifier.background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 404) -@Composable -private fun ModalNotificationContentPreview_Dark( - @PreviewParameter(GlobalNotificationProvider::class) param: ModalNotification, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ModalNotificationContent( notification = param, modifier = Modifier.background( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 050f08d79e..f03dc73425 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -12,14 +12,17 @@ import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.userwallets.UserWalletIdBuilder -import com.tangem.tap.* +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.saveWallet.redux.SaveWalletAction +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber @@ -79,6 +82,8 @@ object OnboardingHelper { ) { Analytics.setContext(scanResponse) scope.launch { + val settingsRepository = store.inject(DaggerGraphState::settingsRepository) + when { // When should save user wallets, then save card without navigate to save wallet screen store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { @@ -90,16 +95,11 @@ object OnboardingHelper { ), ) - val toggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - if (toggles.isGeneralManagerEnabled) { - store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError)) - } else { - store.dispatchWithMain(SaveWalletAction.Save) - } + store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError)) } // When should not save user wallets but device has biometry and save wallet screen has not been shown, // then open save wallet screen - tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> { + tangemSdkManager.canUseBiometry && settingsRepository.shouldShowSaveUserWalletScreen() -> { proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) delay(timeMillis = 1_200) @@ -142,7 +142,8 @@ object OnboardingHelper { backupCardsIds: List?, hasBackupError: Boolean, ) { - val userWallet = UserWalletBuilder(scanResponse = scanResponse) + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) .hasBackupError(hasBackupError) .backupCardsIds(backupCardsIds?.toSet()) .build() @@ -151,6 +152,7 @@ object OnboardingHelper { return } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index ca78c542d3..ef5296a6b4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -23,12 +23,14 @@ import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.mainScope +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware @@ -120,8 +122,10 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch val walletManager = if (noteState.walletManager != null) { noteState.walletManager } else { - val wmFactory = globalState.tapWalletManager.walletManagerFactory - val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard { + val wmFactory = runBlocking { + store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() + } + val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard { val message = "Loading cancelled. Cause: wallet manager didn't created" val customError = TapError.CustomError(message) store.dispatchErrorNotification(customError) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index 4aa4e8a4d4..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..b59d2efa0b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -12,7 +12,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.asLockable import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding @@ -32,10 +32,10 @@ import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.userWalletsListManager import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware @@ -60,6 +60,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager val twinCardsState = store.state.twinCardsState + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) fun getScanResponse(): ScanResponse { return when (twinCardsState.mode) { @@ -244,8 +245,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val walletManager = if (twinCardsState.walletManager != null) { twinCardsState.walletManager } else { - val wmFactory = globalState.tapWalletManager.walletManagerFactory - val walletManager = wmFactory.makePrimaryWalletManager(getScanResponse()).guard { + val wmFactory = runBlocking { + store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() + } + val walletManager = wmFactory?.makePrimaryWalletManager(getScanResponse()).guard { val message = "Loading cancelled. Cause: wallet manager didn't created" val customError = TapError.CustomError(message) store.dispatchErrorNotification(customError) @@ -363,6 +366,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } private fun getPopBackScreen(): AppScreen { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + return if (userWalletsListManager.hasUserWallets) { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } .fold(onSuccess = { true }, onFailure = { false }) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index 75e54a693c..b4260dd2e8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -15,10 +15,10 @@ 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 +import com.tangem.domain.wallets.models.Artwork import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.* @@ -88,13 +88,13 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( binding.toolbar.title = getText(R.string.twins_recreate_toolbar) - mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) { + mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1_URL) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) fallback(R.drawable.card_placeholder_black) } - mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) { + mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2_URL) { placeholder(R.drawable.card_placeholder_white) error(R.drawable.card_placeholder_white) fallback(R.drawable.card_placeholder_white) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 5c29c96fb9..925dc9bcc2 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -11,13 +11,12 @@ import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.Artwork -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.models.Artwork +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator @@ -418,15 +417,11 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) when (val error = result.error) { is TangemSdkError.BackupFailedNotEmptyWallets -> { - if (card?.canSkipBackup == false) { - store.dispatchOnMain( - GlobalAction.ShowDialog( - BackupDialog.ResetBackupCard(error.cardId), - ), - ) - } else { - crashlytics.recordException(error) - } + store.dispatchOnMain( + GlobalAction.ShowDialog( + BackupDialog.ResetBackupCard(error.cardId), + ), + ) } is TangemSdkError.IssuerSignatureLoadingFailed -> { store.dispatchOnMain( @@ -497,7 +492,17 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)) } } - is CompletionResult.Failure -> Unit + is CompletionResult.Failure -> { + when (val error = result.error) { + is TangemSdkError.BackupFailedNotEmptyWallets -> { + store.dispatchOnMain( + GlobalAction.ShowDialog( + BackupDialog.ResetBackupCard(error.cardId), + ), + ) + } + } + } } } } @@ -543,7 +548,8 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } if (scanResponse != null) { - val userWallet = UserWalletBuilder(scanResponse) + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) .backupCardsIds(backupState.backupCardIds.toSet()) .build() .guard { @@ -551,6 +557,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) return@launch } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.update( userWalletId = userWallet.walletId, update = { wallet -> @@ -562,7 +569,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) ) }, ) - store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(userWalletsListManager)) } val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt index ff18f45362..e79ea8b9fe 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt @@ -11,13 +11,11 @@ internal sealed interface SaveWalletAction : Action { val backupCardsIds: Set?, ) : SaveWalletAction - data object Save : SaveWalletAction { + data object AllowToUseBiometrics : SaveWalletAction { data object Success : SaveWalletAction data class Error(val error: TangemError) : SaveWalletAction } - data object AllowToUseBiometrics : SaveWalletAction - data object Dismiss : SaveWalletAction data object CloseError : SaveWalletAction @@ -26,7 +24,5 @@ internal sealed interface SaveWalletAction : Action { data object Cancel : SaveWalletAction } - data object SaveWalletWasShown : SaveWalletAction - data class SaveWalletAfterBackup(val hasBackupError: Boolean) : SaveWalletAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 5f110e31a9..45eb5225a2 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -6,10 +6,8 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.userwallets.UserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding @@ -18,9 +16,11 @@ import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch @@ -46,16 +46,14 @@ internal class SaveWalletMiddleware { private fun handleAction(action: SaveWalletAction, state: SaveWalletState) { when (action) { - is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state) is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state) is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics() - is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown() is SaveWalletAction.Dismiss -> dismiss(state) is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(state, action.hasBackupError) - is SaveWalletAction.Save.Success, + is SaveWalletAction.AllowToUseBiometrics.Success, + is SaveWalletAction.AllowToUseBiometrics.Error, is SaveWalletAction.ProvideBackupInfo, is SaveWalletAction.CloseError, - is SaveWalletAction.Save.Error, is SaveWalletAction.EnrollBiometrics, is SaveWalletAction.EnrollBiometrics.Cancel, -> Unit @@ -66,7 +64,8 @@ internal class SaveWalletMiddleware { scope.launch { val backupInfo = state.backupInfo ?: error("Backup info is null") - val userWallet = UserWalletBuilder(backupInfo.scanResponse) + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(backupInfo.scanResponse, walletNameGenerateUseCase) .backupCardsIds(state.backupInfo.backupCardsIds) .hasBackupError(hasBackupError) .build() @@ -75,6 +74,7 @@ internal class SaveWalletMiddleware { return@launch } + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) .flatMap { saveAccessCodeIfNeeded(accessCode = backupInfo.accessCode, cardsInWallet = userWallet.cardsInWallet) @@ -91,82 +91,6 @@ internal class SaveWalletMiddleware { store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) } - private fun saveWalletIfBiometricsEnrolled(state: SaveWalletState) { - if (tangemSdkManager.needEnrollBiometrics) { - store.dispatchOnMain(SaveWalletAction.EnrollBiometrics) - } else { - saveWallet(state) - } - } - - /** - - * or from [SaveWalletState.backupInfo] if provided from - * [com.tangem.tap.features.onboarding.OnboardingHelper.trySaveWalletAndNavigateToWalletScreen] - * - * If saved user's wallet was selected then pop back to [AppScreen.Wallet] - * or navigate to [AppScreen.WalletSelector] otherwise - * - * TODO: Update that logic after onboarding and backup features refactoring - * */ - private fun saveWallet(state: SaveWalletState) { - val scanResponse = state.backupInfo?.scanResponse - ?: store.state.globalState.scanResponse - ?: return - - if (state.backupInfo != null) { - // TODO: Remove after onboarding refactoring - Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On)) - } else { - Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) - } - - scope.launch { - val userWallet = userWalletsListManager.selectedUserWalletSync - ?: UserWalletBuilder(scanResponse) - .backupCardsIds(state.backupInfo?.backupCardsIds) - .build() - ?: return@launch - - val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles) - if (!featureToggles.isGeneralManagerEnabled) { - provideLockableUserWalletsListManagerIfNot() - } - - val isFirstSavedWallet = !userWalletsListManager.hasUserWallets - - saveAccessCodeIfNeeded(accessCode = state.backupInfo?.accessCode, cardsInWallet = userWallet.cardsInWallet) - .flatMap { - // Save wallet only at first time (SaveWalletBottomSheet). - // Otherwise (Example, add new wallet in Details) userWalletsListManager.wallets subscribers will - // receive useless updates. - // See: OnboardingHelper.trySaveWalletAndNavigateToWalletScreen() - if (isFirstSavedWallet) { - userWalletsListManager.save(userWallet, canOverride = true) - } else { - CompletionResult.Success(Unit) - } - } - .doOnFailure { error -> - store.dispatchWithMain(SaveWalletAction.Save.Error(error)) - } - .doOnSuccess { - store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) - - // Enable saving access codes only if this is the first time user save the wallet - if (isFirstSavedWallet) { - preferencesStorage.shouldSaveAccessCodes = true - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, - ) - } - - store.dispatchOnMain(SaveWalletAction.Save.Success) - store.navigateToWallet() - } - }.saveIn(saveWalletJobHolder) - } - private fun allowToUseBiometrics(state: SaveWalletState) { if (tangemSdkManager.needEnrollBiometrics) { store.dispatchOnMain(SaveWalletAction.EnrollBiometrics) @@ -185,10 +109,13 @@ internal class SaveWalletMiddleware { * because it will be automatically saved on UserWalletsListManager switch */ + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { val error = IllegalStateException("No selected user wallet") Timber.e(error, "Unable to save user wallet") - store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) + store.dispatchWithMain( + SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)), + ) return@launch } @@ -198,29 +125,17 @@ internal class SaveWalletMiddleware { private suspend fun handleSuccessAllowing(userWallet: UserWallet) { store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) - preferencesStorage.shouldSaveAccessCodes = true + + store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true) + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( isBiometricsRequestPolicy = userWallet.hasAccessCode, ) - store.dispatchWithMain(SaveWalletAction.Save.Success) + store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success) store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) } - private suspend fun provideLockableUserWalletsListManagerIfNot() { - if (store.state.globalState.userWalletsListManager?.isLockable() == true) return - - val context = foregroundActivityObserver.foregroundActivity?.applicationContext.guard { - val error = IllegalStateException("No activities in foreground") - Timber.e(error) - store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) - return - } - val manager = UserWalletsListManager.provideBiometricImplementation(context) - - store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) - } - private fun dismiss(state: SaveWalletState) { if (state.backupInfo != null) { // TODO: Remove after onboarding refactoring @@ -230,10 +145,6 @@ internal class SaveWalletMiddleware { } } - private fun saveWalletWasShown() { - preferencesStorage.shouldShowSaveUserWalletScreen = false - } - private suspend fun saveAccessCodeIfNeeded( accessCode: String?, cardsInWallet: Set, diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt index 88b5697eca..370f7cd935 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt @@ -21,14 +21,13 @@ internal object SaveWalletReducer { backupCardsIds = action.backupCardsIds, ), ) - is SaveWalletAction.Save, is SaveWalletAction.AllowToUseBiometrics, -> state.copy(isSaveInProgress = true) - is SaveWalletAction.Save.Error -> state.copy( + is SaveWalletAction.AllowToUseBiometrics.Error -> state.copy( error = action.error, isSaveInProgress = false, ) - is SaveWalletAction.Save.Success -> state.copy( + is SaveWalletAction.AllowToUseBiometrics.Success -> state.copy( backupInfo = null, isSaveInProgress = false, ) @@ -47,7 +46,6 @@ internal object SaveWalletReducer { needEnrollBiometrics = false, isSaveInProgress = false, ) - is SaveWalletAction.SaveWalletWasShown, is SaveWalletAction.SaveWalletAfterBackup, -> state } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt index fe5792ad29..d0d7c64fb5 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt @@ -11,9 +11,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent @@ -25,7 +25,7 @@ import javax.inject.Inject internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies override val expandedHeightFraction: Float = .98f diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt index 99947493fc..40164976d3 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -1,9 +1,10 @@ package com.tangem.tap.features.saveWallet.ui import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles +import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.details.ui.cardsettings.TextReference @@ -11,34 +12,36 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog import com.tangem.tap.store +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.rekotlin.StoreSubscriber import javax.inject.Inject @HiltViewModel internal class SaveWalletViewModel @Inject constructor( - private val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, + private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, + dispatchers: AppCoroutineDispatcherProvider, ) : ViewModel(), StoreSubscriber { + private val stateInternal = MutableStateFlow(SaveWalletScreenState()) val state: StateFlow = stateInternal init { subscribeToStoreChanges() - store.dispatchOnMain(SaveWalletAction.SaveWalletWasShown) + + viewModelScope.launch(dispatchers.main) { + setSaveWalletScreenShownUseCase() + } } fun saveWallet() { analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) - - if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) { - store.dispatch(SaveWalletAction.AllowToUseBiometrics) - } else { - store.dispatch(SaveWalletAction.Save) - } + store.dispatch(SaveWalletAction.AllowToUseBiometrics) } fun cancelOrClose() { diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt index 9113d5001d..115bd8d273 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.saveWallet.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable @@ -9,6 +10,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @@ -40,17 +42,10 @@ private fun EnrollBiometricDialogContentSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun EnrollBiometricDialogContentPreview_Light() { - TangemTheme { - EnrollBiometricDialogContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun EnrollBiometricDialogContentPreview_Dark() { - TangemTheme(isDark = true) { +private fun EnrollBiometricDialogContentPreview() { + TangemThemePreview { EnrollBiometricDialogContentSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt index a8bbc46f02..c7679a466d 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.saveWallet.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -27,6 +28,7 @@ import com.tangem.core.ui.components.SpacerHHalf import com.tangem.core.ui.components.SpacerW24 import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @@ -196,17 +198,10 @@ private fun SaveWalletScreenContentSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SaveWalletScreenContentPreview_Light() { - TangemTheme { - SaveWalletScreenContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SaveWalletScreenContentPreview_Dark() { - TangemTheme(isDark = true) { +private fun SaveWalletScreenContentPreview() { + TangemThemePreview { SaveWalletScreenContentSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index cfdcf74a19..e3096c6a4f 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -1,19 +1,21 @@ package com.tangem.tap.features.send.redux.middlewares -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero -import com.tangem.tap.common.redux.AppState import com.tangem.domain.demo.DemoTransactionSender +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.AmountActionUi import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.ReceiptAction import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.send.redux.states.SendState +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers @@ -27,6 +29,7 @@ import java.math.BigDecimal */ class RequestFeeMiddleware { + @Suppress("CyclomaticComplexMethod") fun handle(appState: AppState?, dispatch: DispatchFunction) { val sendState = appState?.sendState ?: return val walletManager = sendState.walletManager ?: return @@ -45,7 +48,7 @@ class RequestFeeMiddleware { val txSender = if (scanResponse.isDemoCard()) { DemoTransactionSender(walletManager) } else { - walletManager as TransactionSender + walletManager } scope.launch { val feeResult = txSender.getFee(destinationAmount, destinationAddress) @@ -73,12 +76,17 @@ class RequestFeeMiddleware { dispatch(FeeAction.FeeCalculation.ClearResult) dispatch(FeeAction.ChangeLayoutVisibility(main = false)) - store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( - walletManager = walletManager, - amountToSend = destinationAmount, - feeAmount = null, - destinationAddress = destinationAddress, - ) + val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) + if (featureToggles.isLocalLogsEnabled) { + saveBlockchainError(feeResult, destinationAddress, destinationAmount, walletManager) + } else { + store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( + walletManager = walletManager, + amountToSend = destinationAmount, + feeAmount = null, + destinationAddress = destinationAddress, + ) + } val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext dispatch( @@ -93,4 +101,28 @@ class RequestFeeMiddleware { } } } + + private fun saveBlockchainError( + feeResult: Result.Failure, + destinationAddress: String, + destinationAmount: Amount, + walletManager: WalletManager, + ) { + store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( + error = BlockchainErrorInfo( + errorMessage = (feeResult.error as? BlockchainSdkError)?.customMessage + ?: "It isn't BlockchainSdkError", + blockchainId = walletManager.wallet.blockchain.id, + derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "", + destinationAddress = destinationAddress, + tokenSymbol = if (destinationAmount.type is AmountType.Token) { + destinationAmount.currencySymbol + } else { + "" + }, + amount = destinationAmount.value?.stripZeroPlainString() ?: "0", + fee = null, + ), + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index e85db98c7b..9067bffe39 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -4,7 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras @@ -12,14 +12,16 @@ 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.demo.DemoTransactionSender +import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.tap.common.analytics.events.Token @@ -31,7 +33,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.domain.demo.DemoTransactionSender import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.FeeAction.RequestFee @@ -170,11 +171,7 @@ private fun sendTransaction( transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) } transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) } transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) } - transactionExtras.hederaMemoState?.memo?.let { - txData = txData.copy( - extras = HederaTransactionBuilder.HederaTransactionExtras(it), - ) - } + transactionExtras.hederaMemoState?.memo?.let { txData = txData.copy(extras = HederaTransactionExtras(it)) } transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) } scope.launch { @@ -278,6 +275,7 @@ private fun sendTransaction( } is SimpleResult.Failure -> { updateFeedbackManagerInfo( + sendResult = sendResult.error, walletManager = walletManager, amountToSend = amountToSend, feeAmount = fee.amount, @@ -368,13 +366,33 @@ private fun updateFeedbackManagerInfo( amountToSend: Amount, feeAmount: Amount, destinationAddress: String, + sendResult: BlockchainError, ) { - store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( - walletManager = walletManager, - amountToSend = amountToSend, - feeAmount = feeAmount, - destinationAddress = destinationAddress, - ) + val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles) + if (featureToggles.isLocalLogsEnabled) { + store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( + error = BlockchainErrorInfo( + errorMessage = (sendResult as? BlockchainSdkError)?.customMessage ?: "It isn't BlockchainSdkError", + blockchainId = walletManager.wallet.blockchain.id, + derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "", + destinationAddress = destinationAddress, + tokenSymbol = if (amountToSend.type is AmountType.Token) { + amountToSend.currencySymbol + } else { + "" + }, + amount = amountToSend.value?.stripZeroPlainString() ?: "0", + fee = feeAmount.value?.stripZeroPlainString() ?: "0", + ), + ) + } else { + store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError( + walletManager = walletManager, + amountToSend = amountToSend, + feeAmount = feeAmount, + destinationAddress = destinationAddress, + ) + } } fun createValidateTransactionError( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt index f036004238..dcacf9fef2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt @@ -3,11 +3,11 @@ package com.tangem.tap.features.tokens.impl.data import androidx.paging.PagingSource import androidx.paging.PagingState import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.isSupportedInApp +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.common.extensions.isSupportedInApp import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt index 2bffd45d01..2d620d45e8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/CoinsResponseConverter.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.tokens.impl.data.converters import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.isSupportedInApp import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.utils.converter.Converter diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt index d1babd19b7..561c8c123a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/converters/TestnetTokensConfigConverter.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.tokens.impl.data.converters import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.local.testnet.models.TestnetTokensConfig -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.utils.converter.Converter diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt index a6f68a2097..272c773c95 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel import dagger.hilt.android.AndroidEntryPoint @@ -23,7 +23,7 @@ import javax.inject.Inject internal class TokensListFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Composable override fun ScreenContent(modifier: Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt index 806c161a01..12620898b6 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -22,6 +23,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import kotlinx.collections.immutable.ImmutableCollection @@ -135,9 +137,10 @@ private fun RowScope.NetworkTitle(model: NetworkItemState) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_DetailedNetworksList_ManageAccess_Light() { - TangemTheme { +private fun Preview_DetailedNetworksList_ManageAccess() { + TangemThemePreview { DetailedNetworksList( isExpanded = true, token = TokenListPreviewData.createManageToken(), @@ -147,33 +150,10 @@ private fun Preview_DetailedNetworksList_ManageAccess_Light() { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_DetailedNetworksList_ManageAccess_Dark() { - TangemTheme { - DetailedNetworksList( - isExpanded = true, - token = TokenListPreviewData.createManageToken(), - networks = TokenListPreviewData.createManageNetworksList(), - ) - } -} - -@Preview -@Composable -private fun Preview_DetailedNetworksList_ReadAccess_Light() { - TangemTheme { - DetailedNetworksList( - isExpanded = true, - token = TokenListPreviewData.createReadToken(), - networks = TokenListPreviewData.createReadNetworksList(), - ) - } -} - -@Preview -@Composable -private fun Preview_DetailedNetworksList_ReadAccess_Dark() { - TangemTheme { +private fun Preview_DetailedNetworksList_ReadAccess() { + TangemThemePreview { DetailedNetworksList( isExpanded = true, token = TokenListPreviewData.createReadToken(), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt index 671961ab61..b51b65f1c0 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Icon @@ -9,6 +10,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @@ -39,25 +41,10 @@ internal fun NetworkItemArrow(itemHeight: Dp, isLastItem: Boolean) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NetworkItemArrow_Column_Light() { - TangemTheme(isDark = false) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(color = TangemTheme.colors.background.primary) - .padding(start = TangemTheme.dimens.size36), - ) { - NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = false) - NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = true) - } - } -} - -@Preview -@Composable -private fun Preview_NetworkItemArrow_Column_Dark() { - TangemTheme(isDark = true) { +private fun Preview_NetworkItemArrow_Column() { + TangemThemePreview { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt index bae044a2a6..ce1967b48b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme @@ -24,6 +25,7 @@ import androidx.constraintlayout.compose.Dimension import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.CurrencyPlaceholderIcon +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState @@ -216,33 +218,19 @@ private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, m } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenItem_ManageAccess_Light() { - TangemTheme(isDark = false) { - TokenItem(model = TokenListPreviewData.createManageToken()) - } -} - -@Preview() -@Composable -private fun Preview_TokenItem_ManageAccess_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TokenItem_ManageAccess() { + TangemThemePreview { TokenItem(model = TokenListPreviewData.createManageToken()) } } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenItem_ReadAccess_Light() { - TangemTheme(isDark = false) { - TokenItem(model = TokenListPreviewData.createReadToken()) - } -} - -@Preview() -@Composable -private fun Preview_TokenItem_ReadAccess_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TokenItem_ReadAccess() { + TangemThemePreview { TokenItem(model = TokenListPreviewData.createReadToken()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 70335b9609..56366dfb51 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.Crossfade import androidx.compose.foundation.background @@ -34,6 +35,7 @@ import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder @@ -193,21 +195,12 @@ private fun SaveChangesButton(showProgress: Boolean, onClick: () -> Unit, modifi } @Preview(showSystemUi = true) +@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokensListScreen_Light( +private fun Preview_TokensListScreen( @PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder, ) { - TangemTheme(isDark = false) { - TokensListScreen(stateHolder) - } -} - -@Preview(showSystemUi = true) -@Composable -private fun Preview_TokensListScreen_Dark( - @PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokensListScreen(stateHolder) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt index 71aba31a33..5e0aafc999 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.InputField import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.Title @@ -171,7 +172,7 @@ private fun Hint(value: String) { @Preview @Composable private fun Preview_AddTokensToolbar_EditAccess() { - TangemTheme { + TangemThemePreview { TokensListToolbar( state = Title.Manage( titleResId = R.string.main_manage_tokens, @@ -186,7 +187,7 @@ private fun Preview_AddTokensToolbar_EditAccess() { @Preview @Composable private fun Preview_AddTokensToolbar_ReadAccess() { - TangemTheme { + TangemThemePreview { TokensListToolbar( state = Title.Read( titleResId = R.string.common_search_tokens, @@ -202,7 +203,7 @@ private fun Preview_AddTokensToolbar_ReadAccess() { private fun Preview_AddTokensToolbar_SearchInputField() { var value by remember { mutableStateOf("") } - TangemTheme { + TangemThemePreview { TokensListToolbar( state = InputField( onBackButtonClick = {}, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 0fa1976240..53867da367 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.* import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction @@ -19,7 +20,6 @@ import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.canHandleBlockchain import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index dd239f1135..1f1c3132a0 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -13,16 +13,19 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable -import com.tangem.tap.* +import com.tangem.tap.backupService import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -83,6 +86,7 @@ internal class WelcomeMiddleware { """.trimIndent(), ) + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.unlockIfLockable(type = UnlockType.ANY) .doOnFailure { error -> Timber.e(error, "Unable to unlock user wallets with biometrics") @@ -113,8 +117,11 @@ internal class WelcomeMiddleware { ) scanCardInternal { scanResponse -> - val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal + val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build() + ?: return@scanCardInternal + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) .doOnFailure { error -> Timber.e(error, "Unable to save user wallet") @@ -140,12 +147,14 @@ internal class WelcomeMiddleware { ) Analytics.addContext(scanResponse) if (currency != null) { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + Analytics.send( event = Basic.SignedIn( currency = currency, batch = scanResponse.card.batchId, signInType = signInType, - walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(), + walletsCount = userWalletsListManager.walletsCount.toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) @@ -153,6 +162,7 @@ internal class WelcomeMiddleware { } private suspend fun disableUserWalletsSaving() { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.clear() .flatMap { tangemSdkManager.clearSavedUserCodes() } .doOnFailure { e -> @@ -165,9 +175,12 @@ internal class WelcomeMiddleware { } private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + isBiometricsRequestPolicy = shouldSaveAccessCodes, ) + store.inject(DaggerGraphState::scanCardProcessor).scan( analyticsSource = AnalyticsParam.ScreensSources.SignIn, onSuccess = { scanResponse -> diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index c3197818c0..60ab61900f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -15,10 +15,10 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.SignIn import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -31,7 +31,7 @@ import javax.inject.Inject internal class WelcomeFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies override fun onStart() { super.onStart() diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index 1f4b9e650e..bc672b7c2f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -31,14 +31,12 @@ internal class WelcomeViewModel @Inject constructor( private val stateInternal = MutableStateFlow(WelcomeScreenState()) val state: StateFlow = stateInternal - init { + override fun onCreate(owner: LifecycleOwner) { store.dispatch(WelcomeAction.SetCoroutineScope(viewModelScope)) subscribeToStoreChanges() initGlobalState() - } - override fun onCreate(owner: LifecycleOwner) { val welcomeAction = if (initialIntent != null) { WelcomeAction.ProceedWithIntent(initialIntent) } else { @@ -81,9 +79,10 @@ internal class WelcomeViewModel @Inject constructor( } } - override fun onCleared() { + override fun onDestroy(owner: LifecycleOwner) { store.dispatch(WelcomeAction.ClearCoroutineScope) store.unsubscribe(this) + super.onDestroy(owner) } private fun createWarningIfNeeded(error: TangemError?): WarningModel? { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt index ca9a8d5769..a6315d5b1e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.ui.components +import android.content.res.Configuration import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -7,7 +8,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.wallet.R @@ -72,17 +73,10 @@ private fun BiometricsLockoutDialogSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BiometricsLockoutDialogPreview_Light() { - TangemTheme { - BiometricsLockoutDialogSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialogPreview_Dark() { - TangemTheme(isDark = true) { +private fun BiometricsLockoutDialogPreview() { + TangemThemePreview { BiometricsLockoutDialogSample() } } @@ -100,17 +94,10 @@ private fun BiometricsLockoutDialog_Permanent_Sample(modifier: Modifier = Modifi } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BiometricsLockoutDialog_Permanent_Preview_Light() { - TangemTheme { - BiometricsLockoutDialog_Permanent_Sample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricsLockoutDialog_Permanent_Preview_Dark() { - TangemTheme(isDark = true) { +private fun BiometricsLockoutDialog_Permanent_Preview() { + TangemThemePreview { BiometricsLockoutDialog_Permanent_Sample() } } @@ -123,17 +110,10 @@ private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun KeyInvalidatedWarningPreview_Light() { - TangemTheme { - KeyInvalidatedWarningSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun KeyInvalidatedWarningPreview_Dark() { - TangemTheme(isDark = true) { +private fun KeyInvalidatedWarningPreview() { + TangemThemePreview { KeyInvalidatedWarningSample() } } @@ -146,17 +126,10 @@ private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun BiometricDisabledWarningPreview_Light() { - TangemTheme { - BiometricDisabledWarningSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun BiometricDisabledWarningPreview_Dark() { - TangemTheme(isDark = true) { +private fun BiometricDisabledWarningPreview() { + TangemThemePreview { BiometricDisabledWarningSample() } } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt index cc83e0d893..5aca214e93 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Icon @@ -12,6 +13,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @@ -97,17 +99,10 @@ private fun WelcomeScreenContentSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun WelcomeScreenContentPreview_Light() { - TangemTheme { - WelcomeScreenContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun WelcomeScreenContentPreview_Dark() { - TangemTheme(isDark = true) { +private fun WelcomeScreenContentPreview() { + TangemThemePreview { WelcomeScreenContentSample() } } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 27ac9a4336..16427253e8 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -1,7 +1,7 @@ package com.tangem.tap.network.auth import com.tangem.common.extensions.toHexString -import com.tangem.lib.auth.AuthProvider +import com.tangem.datasource.api.common.AuthProvider import com.tangem.tap.proxy.AppStateHolder internal class DefaultAuthProvider(private val appStateHolder: AppStateHolder) : AuthProvider { diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 0185472a6e..06b2edda3a 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -1,9 +1,9 @@ package com.tangem.tap.network.auth.di +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.lib.auth.AppVersionProvider -import com.tangem.lib.auth.AuthProvider import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.tap.network.auth.DefaultAppVersionProvider import com.tangem.tap.network.auth.DefaultAuthProvider diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt index 7ed890dda8..041b9561ee 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt @@ -5,7 +5,9 @@ import com.tangem.blockchain.common.Token import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.converter.TwoWayConverter @@ -20,9 +22,7 @@ internal class CryptoCurrencyConverter : TwoWayConverter - get() = _userWalletsListManagerFlow - - private val _userWalletsListManagerFlow = MutableStateFlow(null) +class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolder { @Deprecated("Use scan response from selected user wallet") var scanResponse: ScanResponse? = null diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 8b3e444c59..a04a551fc2 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -8,7 +8,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras @@ -21,17 +21,17 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.externallinkprovider.TxExploreState import com.tangem.blockchain.network.ResultChecker +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner -import com.tangem.tap.userWalletsListManager import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -42,6 +42,7 @@ class TransactionManagerImpl( private val appStateHolder: AppStateHolder, private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, + private val userWalletsListManager: UserWalletsListManager, ) : TransactionManager { override suspend fun sendApproveTransaction( @@ -140,7 +141,7 @@ class TransactionManagerImpl( Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } Blockchain.Cosmos -> CosmosTransactionExtras(memo) Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo) + Blockchain.Hedera -> HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) else -> null } @@ -223,26 +224,33 @@ class TransactionManagerImpl( when (fee.data) { is TransactionFee.Single -> { val normalFee = (fee.data as TransactionFee.Single).normal - val singleFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - ) - ProxyFees.SingleFee( - singleFee = singleFee, - ) + val singleFee = if (normalFee as? Fee.CardanoToken != null) { + ProxyFee.CardanoToken( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + minAdaValue = normalFee.minAdaValue, + ) + } else { + ProxyFee.Common( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = normalFee.amount), + ) + } + + ProxyFees.SingleFee(singleFee = singleFee) } is TransactionFee.Choosable -> { val choosableFee = fee.data as TransactionFee.Choosable ProxyFees.MultipleFees( - minFee = ProxyFee( + minFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ), - normalFee = ProxyFee( + normalFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ), - priorityFee = ProxyFee( + priorityFee = ProxyFee.Common( gasLimit = BigInteger.ZERO, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ), @@ -299,15 +307,15 @@ class TransactionManagerImpl( is Result.Success -> { val choosableFee = fee.data - val minProxyFee = ProxyFee( + val minProxyFee = ProxyFee.Common( gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) - val normalProxyFee = ProxyFee( + val normalProxyFee = ProxyFee.Common( gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) - val priorityProxyFee = ProxyFee( + val priorityProxyFee = ProxyFee.Common( gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit, fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) @@ -462,7 +470,7 @@ class TransactionManagerImpl( scale = blockchain.decimals(), mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), ) - val minFee = ProxyFee( + val minFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, @@ -470,7 +478,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - val normalFee = ProxyFee( + val normalFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, @@ -478,7 +486,7 @@ class TransactionManagerImpl( decimals = blockchain.decimals(), ), ) - val priorityFee = ProxyFee( + val priorityFee = ProxyFee.Common( gasLimit = gasLimit, fee = ProxyAmount( currencySymbol = blockchain.currency, diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 8d098f79c1..ac5a55104e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -2,85 +2,20 @@ package com.tangem.tap.proxy import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toCoinId -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.lib.crypto.models.Currency -import com.tangem.lib.crypto.models.Currency.NativeToken -import com.tangem.lib.crypto.models.Currency.NonNativeToken import com.tangem.lib.crypto.models.ProxyAmount -import com.tangem.tap.userWalletsListManager import timber.log.Timber import java.math.BigDecimal class UserWalletManagerImpl( private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListManager: UserWalletsListManager, ) : UserWalletManager { - override suspend fun getUserTokens( - networkId: String, - derivationPath: String?, - isExcludeCustom: Boolean, - ): List { - // FIXME: Find user wallet by ID - val userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull) { - "No user wallet selected" - } - return currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWallet.walletId) - .filter { - val checkCustom = if (isExcludeCustom) { - !it.isCustom - } else { - true - } - val blockchain = Blockchain.fromId(it.network.id.value) - - blockchain.toNetworkId() == networkId && - checkCustom && - it.network.derivationPath.value == derivationPath - } - .map { - val blockchain = Blockchain.fromId(it.network.id.value) - - if (it is CryptoCurrency.Token) { - NonNativeToken( - id = it.id.rawCurrencyId ?: "", - name = it.name, - symbol = it.symbol, - networkId = blockchain.toNetworkId(), - contractAddress = it.contractAddress, - decimalCount = it.decimals, - ) - } else { - NativeToken( - id = it.id.rawCurrencyId ?: "", - name = it.name, - symbol = it.symbol, - networkId = blockchain.toNetworkId(), - ) - } - } - } - - override fun getNativeTokenForNetwork(networkId: String): Currency { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - return NativeToken( - id = blockchain.toCoinId(), - name = blockchain.fullName, - symbol = blockchain.currency, - networkId = networkId, - ) - } - override fun getWalletId(): String { val selectedUserWallet = requireNotNull( userWalletsListManager.selectedUserWalletSync, @@ -88,18 +23,6 @@ class UserWalletManagerImpl( return selectedUserWallet.walletId.stringValue } - override suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean { - val blockchain = requireNotNull(Blockchain.fromNetworkId(currency.networkId)) { "blockchain not found" } - return try { - val walletManager = getActualWalletManager(blockchain, derivationPath) - walletManager.cardTokens.any { - it.id == currency.id - } - } catch (e: IllegalArgumentException) { - false - } - } - override suspend fun hideAllTokens() { // FIXME: Used only in Tester Actions Timber.w("Not implemented") @@ -119,37 +42,6 @@ class UserWalletManagerImpl( ?.hash } - override suspend fun getCurrentWalletTokensBalance( - networkId: String, - extraTokens: List, - derivationPath: String?, - ): Map { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - - // workaround for get balance for tokens that doesn't exist in wallet - val extraTokensToLoadBalance = extraTokens - .filterIsInstance() - .map { - it.toSdkToken() - } - .filter { - !walletManager.cardTokens.contains(it) - } - walletManager.addTokens(extraTokensToLoadBalance) - walletManager.update() - val balances = walletManager.wallet.amounts.map { entry -> - val amount = entry.value - amount.currencySymbol to ProxyAmount( - amount.currencySymbol, - amount.value ?: BigDecimal.ZERO, - amount.decimals, - ) - }.toMap() - extraTokensToLoadBalance.forEach { walletManager.removeToken(it) } - return balances - } - override suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain, derivationPath) @@ -179,14 +71,4 @@ class UserWalletManagerImpl( "No wallet manager found" } } -} - -private fun NonNativeToken.toSdkToken(): Token { - return Token( - id = this.id, - name = this.name, - symbol = this.symbol, - contractAddress = this.contractAddress, - decimals = this.decimalCount, - ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index d3f44858b1..37fc1060b4 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -1,9 +1,8 @@ package com.tangem.tap.proxy.di -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.tap.proxy.* @@ -28,13 +27,11 @@ internal object ProxyModule { @Singleton fun provideUserWalletManager( walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, - userWalletsStore: UserWalletsStore, + userWalletsListManager: UserWalletsListManager, ): UserWalletManager { return UserWalletManagerImpl( walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, - userWalletsStore = userWalletsStore, + userWalletsListManager = userWalletsListManager, ) } @@ -44,11 +41,13 @@ internal object ProxyModule { appStateHolder: AppStateHolder, cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, + userWalletsListManager: UserWalletsListManager, ): TransactionManager { return TransactionManagerImpl( appStateHolder = appStateHolder, cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, + userWalletsListManager = userWalletsListManager, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index 995658a42c..f854ba0dae 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -1,8 +1,9 @@ package com.tangem.tap.proxy.redux -import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.core.navigation.email.EmailSender import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter @@ -23,5 +24,6 @@ sealed interface DaggerGraphAction : Action { val cardSdkConfigRepository: CardSdkConfigRepository, val sendRouter: SendRouter, val qrScanningRouter: QrScanningRouter, + val emailSender: EmailSender, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index 7074523df0..b25fdce12b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -22,6 +22,7 @@ object DaggerGraphReducer { cardSdkConfigRepository = action.cardSdkConfigRepository, sendRouter = action.sendRouter, qrScanningRouter = action.qrScanningRouter, + emailSender = action.emailSender, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index c23def20bc..ab600ee8ea 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,8 @@ package com.tangem.tap.proxy.redux import com.tangem.TangemSdkLogger -import com.tangem.blockchain.common.AccountCreator -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.core.navigation.email.EmailSender import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -13,14 +12,16 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.managetokens.navigation.ManageTokensUi @@ -61,14 +62,15 @@ data class DaggerGraphState( val sendRouter: SendRouter? = null, val qrScanningRouter: QrScanningRouter? = null, val currenciesRepository: CurrenciesRepository? = null, - val blockchainDataStorage: BlockchainDataStorage? = null, - val accountCreator: AccountCreator? = null, - val userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles? = null, val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, + val generateWalletNameUseCase: GenerateWalletNameUseCase? = null, val cardRepository: CardRepository? = null, val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null, val tangemSdkLogger: TangemSdkLogger? = null, - val blockchainSDKLogger: BlockchainSDKLogger? = null, + val settingsRepository: SettingsRepository? = null, + val blockchainSDKFactory: BlockchainSDKFactory? = null, + val emailSender: EmailSender? = null, + val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, ) : StateType \ No newline at end of file diff --git a/app/src/mocked/res/values/strings.xml b/app/src/mocked/res/values/strings.xml new file mode 100644 index 0000000000..f13e8b7c5e --- /dev/null +++ b/app/src/mocked/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + Mocked Tangem + + diff --git a/app/src/mocked/res/xml/network_security_config.xml b/app/src/mocked/res/xml/network_security_config.xml new file mode 100644 index 0000000000..52c44ac992 --- /dev/null +++ b/app/src/mocked/res/xml/network_security_config.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index b1e1189c8e..fdf0ed8db2 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -10,7 +10,7 @@ import com.tangem.domain.common.configs.MultiWalletCardConfig import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse -import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest @@ -21,7 +21,7 @@ import org.junit.Test */ internal class DefaultDerivationsRepositoryTest { - private val tangemSdkManager = mockk() + private val tangemSdkManager = mockk() private val userWalletsStore = mockk() private val repository = DefaultDerivationsRepository( tangemSdkManager = tangemSdkManager, diff --git a/build.gradle.kts b/build.gradle.kts index 8ceb8869c1..d92b770060 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,6 +8,7 @@ plugins { alias(deps.plugins.hilt.android) apply false alias(deps.plugins.google.services) apply false alias(deps.plugins.firebase.crashlytics) apply false + alias(deps.plugins.room) apply false } val clean by tasks.registering { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 80517d7175..b77078a426 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -3,46 +3,46 @@ package com.tangem.core.analytics.models sealed class AnalyticsParam { sealed class CardBalanceState(val value: String) { - object Empty : CardBalanceState("Empty") - object Full : CardBalanceState("Full") - object CustomToken : CardBalanceState("Custom Token") - object BlockchainError : CardBalanceState("Blockchain Error") - object NoRate : CardBalanceState("No Rate") + data object Empty : CardBalanceState("Empty") + data object Full : CardBalanceState("Full") + data object CustomToken : CardBalanceState("Custom Token") + data object BlockchainError : CardBalanceState("Blockchain Error") + data object NoRate : CardBalanceState("No Rate") companion object } sealed class TokenBalanceState(val value: String) { - object Empty : TokenBalanceState("Empty") - object Full : TokenBalanceState("Full") + data object Empty : TokenBalanceState("Empty") + data object Full : TokenBalanceState("Full") } sealed class RateApp(val value: String) { - object Liked : RateApp("Liked") - object Disliked : RateApp("Disliked") - object Closed : RateApp("Close") + data object Liked : RateApp("Liked") + data object Disliked : RateApp("Disliked") + data object Closed : RateApp("Close") } sealed class OnOffState(val value: String) { - object On : OnOffState("On") - object Off : OnOffState("Off") + data object On : OnOffState("On") + data object Off : OnOffState("Off") } sealed class OrganizeSortType(val value: String) { - object ByBalance : OrganizeSortType("By Balance") - object Manually : OrganizeSortType("Manually") + data object ByBalance : OrganizeSortType("By Balance") + data object Manually : OrganizeSortType("Manually") } sealed class UserCode(val value: String) { - object AccessCode : UserCode("Access Code") - object Passcode : UserCode("Passcode") + data object AccessCode : UserCode("Access Code") + data object Passcode : UserCode("Passcode") } sealed class AccessCodeRecoveryStatus(val value: String) { val key: String = "Status" - object Enabled : AccessCodeRecoveryStatus("Enabled") - object Disabled : AccessCodeRecoveryStatus("Disabled") + data object Enabled : AccessCodeRecoveryStatus("Enabled") + data object Disabled : AccessCodeRecoveryStatus("Disabled") companion object { fun from(enabled: Boolean): AccessCodeRecoveryStatus { @@ -52,9 +52,9 @@ sealed class AnalyticsParam { } sealed class Error(val value: String) { - object App : Error("App Error") - object CardSdk : Error("Card Sdk Error") - object BlockchainSdk : Error("Blockchain Sdk Error") + data object App : Error("App Error") + data object CardSdk : Error("Card Sdk Error") + data object BlockchainSdk : Error("Blockchain Sdk Error") } sealed class ScreensSources(val value: String) { @@ -86,8 +86,8 @@ sealed class AnalyticsParam { val permissionType: String, ) : TxSentFrom("Approve"), TxData - object WalletConnect : TxSentFrom("WalletConnect") - object Sell : TxSentFrom("Sell") + data object WalletConnect : TxSentFrom("WalletConnect") + data object Sell : TxSentFrom("Sell") } sealed interface TxData { @@ -117,13 +117,13 @@ sealed class AnalyticsParam { } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType("Private key") - object NewSeed : WalletCreationType("New seed") - object SeedImport : WalletCreationType("Seed import") + data object PrivateKey : WalletCreationType("Private key") + data object NewSeed : WalletCreationType("New seed") + data object SeedImport : WalletCreationType("Seed import") } sealed class WalletType(val value: String) { - object MultiCurrency : WalletType(value = "Multicurrency") + data object MultiCurrency : WalletType(value = "Multicurrency") class SingleCurrency(currencyName: String) : WalletType(currencyName) } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index e4239e40d6..26d30378a2 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -3,11 +3,16 @@ plugins { alias(deps.plugins.kotlin.android) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) + alias(deps.plugins.room) id("configuration") } android { namespace = "com.tangem.datasource" + + room { + schemaDirectory("$projectDir/schemas") + } } dependencies { @@ -39,6 +44,7 @@ dependencies { /** Network */ implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(deps.moshi.adapters) implementation(deps.okHttp) implementation(deps.okHttp.prettyLogging) implementation(deps.retrofit) @@ -53,10 +59,14 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) + mockedImplementation(deps.chuckerStub) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) /** Local storages */ implementation(deps.androidx.datastore) + implementation(deps.room.runtime) + implementation(deps.room.ktx) + kapt(deps.room.compiler) } \ No newline at end of file diff --git a/core/datasource/schemas/com.tangem.datasource.local.db.TangemDatabase/1.json b/core/datasource/schemas/com.tangem.datasource.local.db.TangemDatabase/1.json new file mode 100644 index 0000000000..3efb2a6a01 --- /dev/null +++ b/core/datasource/schemas/com.tangem.datasource.local.db.TangemDatabase/1.json @@ -0,0 +1,304 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "a8a710af25033ee27e5043d001385234", + "entities": [ + { + "tableName": "UserWalletEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `artworkUrl` TEXT NOT NULL, `isMultiCurrency` INTEGER NOT NULL, `hasBackupError` INTEGER NOT NULL, `cardsInWallet` TEXT NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artworkUrl", + "columnName": "artworkUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isMultiCurrency", + "columnName": "isMultiCurrency", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasBackupError", + "columnName": "hasBackupError", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "cardsInWallet", + "columnName": "cardsInWallet", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ordinalNumber", + "columnName": "ordinalNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_UserWalletEntity_id", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_UserWalletEntity_ordinalNumber", + "unique": true, + "columnNames": [ + "ordinalNumber" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_UserWalletEntity_ordinalNumber` ON `${TABLE_NAME}` (`ordinalNumber`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "CryptoCurrenciesAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `title` TEXT NOT NULL, `currenciesCount` INTEGER NOT NULL, `isArchived` INTEGER NOT NULL, `ordinalNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userWalletId", + "columnName": "userWalletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currenciesCount", + "columnName": "currenciesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isArchived", + "columnName": "isArchived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ordinalNumber", + "columnName": "ordinalNumber", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_CryptoCurrenciesAccountEntity_id", + "unique": false, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_id` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "index_CryptoCurrenciesAccountEntity_userWalletId", + "unique": false, + "columnNames": [ + "userWalletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrenciesAccountEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)" + } + ], + "foreignKeys": [ + { + "table": "UserWalletEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userWalletId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "CryptoCurrencyEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `currencyBackendId` TEXT, `networkId` TEXT NOT NULL, `accountId` INTEGER NOT NULL, `userWalletId` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `contractAddress` TEXT, `derivationPath` TEXT, FOREIGN KEY(`accountId`) REFERENCES `CryptoCurrenciesAccountEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`userWalletId`) REFERENCES `UserWalletEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currencyBackendId", + "columnName": "currencyBackendId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "networkId", + "columnName": "networkId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userWalletId", + "columnName": "userWalletId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "decimals", + "columnName": "decimals", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contractAddress", + "columnName": "contractAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "derivationPath", + "columnName": "derivationPath", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_CryptoCurrencyEntity_currencyBackendId", + "unique": false, + "columnNames": [ + "currencyBackendId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_currencyBackendId` ON `${TABLE_NAME}` (`currencyBackendId`)" + }, + { + "name": "index_CryptoCurrencyEntity_networkId", + "unique": false, + "columnNames": [ + "networkId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_networkId` ON `${TABLE_NAME}` (`networkId`)" + }, + { + "name": "index_CryptoCurrencyEntity_accountId", + "unique": false, + "columnNames": [ + "accountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_accountId` ON `${TABLE_NAME}` (`accountId`)" + }, + { + "name": "index_CryptoCurrencyEntity_userWalletId", + "unique": false, + "columnNames": [ + "userWalletId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_CryptoCurrencyEntity_userWalletId` ON `${TABLE_NAME}` (`userWalletId`)" + } + ], + "foreignKeys": [ + { + "table": "CryptoCurrenciesAccountEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "UserWalletEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userWalletId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a8a710af25033ee27e5043d001385234')" + ] + } +} diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt similarity index 82% rename from libs/auth/src/main/java/com/tangem/lib/auth/AuthProvider.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index 1323079ec6..a11b7bf574 100644 --- a/libs/auth/src/main/java/com/tangem/lib/auth/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.lib.auth +package com.tangem.datasource.api.common /** * Provides auth for tangemTech API diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b7055574a3..76c7dce8bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -42,6 +42,7 @@ interface TangemExpressApi { @Query("fromContractAddress") fromContractAddress: String, @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, + @Query("fromAddress") fromAddress: String, @Query("toNetwork") toNetwork: String, @Query("fromAmount") fromAmount: String, @Query("fromDecimals") fromDecimals: Int, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 38e872886c..eb9e2a434c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -10,6 +10,7 @@ import retrofit2.http.* * [REDACTED_AUTHOR] */ +@Suppress("TooManyFunctions") interface TangemTechApi { @GET("coins") @@ -76,6 +77,21 @@ interface TangemTechApi { @GET("promotion") suspend fun getPromotionInfo(@Query("programName") name: String): ApiResponse + @GET("settings/{wallet_id}") + suspend fun getUserTokensSettings( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("wallet_id") walletId: String, + ): ApiResponse + + @PUT("settings/{wallet_id}") + suspend fun saveUserTokensSettings( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("wallet_id") walletId: String, + @Body userTokensSettings: UserTokensSettingsResponse, + ): ApiResponse + @POST("user-network-account") suspend fun createUserNetworkAccount( @Header("card_public_key") cardPublicKey: String, @@ -83,6 +99,35 @@ interface TangemTechApi { @Body body: CreateUserNetworkAccountBody, ): ApiResponse + @POST("account") + suspend fun createUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Body body: CreateUserTokensAccountBody, + ): ApiResponse + + @PUT("account/{account_id}") + suspend fun updateUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("account_id") accountId: Int, + @Body body: UpdateUserTokensAccountBody, + ): ApiResponse + + @PUT("account/{account_id}/archive") + suspend fun archiveUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("account_id") accountId: Int, + ): ApiResponse + + @PUT("account/{account_id}/unarchive") + suspend fun restoreUserTokensAccount( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("account_id") accountId: Int, + ): ApiResponse + @GET("features") suspend fun getFeatures(): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt new file mode 100644 index 0000000000..80db96faa7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.tangemTech + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.models.v2.UserTokensResponseV2 +import retrofit2.http.* + +interface TangemTechApiV2 { + + @GET("user-tokens/{wallet_id}") + suspend fun getUserTokens( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("wallet_id") walletId: String, + ): ApiResponse + + @PUT("user-tokens/{wallet_id}") + suspend fun saveUserTokens( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + @Path("wallet_id") walletId: String, + @Body userTokens: UserTokensResponseV2, + ): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt new file mode 100644 index 0000000000..f5a24561f8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechServiceApi.kt @@ -0,0 +1,19 @@ +package com.tangem.datasource.api.tangemTech + +import com.tangem.datasource.config.models.ProviderModel +import retrofit2.http.GET +import retrofit2.http.Header + +/** + * Tangem Tech API for app services + * +[REDACTED_AUTHOR] + */ +interface TangemTechServiceApi { + + @GET("networks/providers") + suspend fun getBlockchainProviders( + @Header("card_public_key") cardPublicKey: String, + @Header("card_id") cardId: String, + ): Map> +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt new file mode 100644 index 0000000000..7f88539dad --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/CreateUserTokensAccountBody.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class CreateUserTokensAccountBody( + @Json(name = "accountId") + val id: Int, + @Json(name = "accountTitle") + val title: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt new file mode 100644 index 0000000000..2413b85d2b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UpdateUserTokensAccountBody.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class UpdateUserTokensAccountBody( + @Json(name = "accountTitle") + val title: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt new file mode 100644 index 0000000000..0e8341612d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensAccountResponse.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class UserTokensAccountResponse( + @Json(name = "accountId") + val id: Int, + @Json(name = "accountTitle") + val title: String, + @Json(name = "archived") + val isArchived: Boolean, + @Json(name = "tokensCount") + val tokensCount: Int? = null, + @Json(name = "tokens") + val tokens: List? = null, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt new file mode 100644 index 0000000000..9d5a0554cf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensSettingsResponse.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +data class UserTokensSettingsResponse( + @Json(name = "group") + val group: UserTokensResponse.GroupType, + @Json(name = "sort") + val sort: UserTokensResponse.SortType, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt new file mode 100644 index 0000000000..fdb8a0df7c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.tangemTech.models.v2 + +import com.squareup.moshi.Json +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse + +data class UserTokensResponseV2( + @Json(name = "accounts") + val accounts: List, +) { + + data class TokensAccount( + @Json(name = "id") + val id: Int, + @Json(name = "title") + val title: String, + @Json(name = "tokens") + val tokens: List? = null, + @Json(name = "tokensCount") + val tokensCount: Int? = null, + @Json(name = "archived") + val isArchived: Boolean, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt new file mode 100644 index 0000000000..042319e1f4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -0,0 +1,87 @@ +package com.tangem.datasource.asset.loader + +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.squareup.moshi.adapter +import com.tangem.datasource.asset.reader.AssetReader +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import timber.log.Timber +import javax.inject.Inject + +/** + * Asset file loader + * +[REDACTED_AUTHOR] + */ +class AssetLoader @Inject constructor( + val assetReader: AssetReader, + @NetworkMoshi val moshi: Moshi, + val dispatchers: CoroutineDispatcherProvider, +) { + + /** Load content [Content] of asset file [fileName] */ + @OptIn(ExperimentalStdlibApi::class) + suspend inline fun load(fileName: String): Content? { + return runCatching(dispatchers.io) { + val json = assetReader.readJson(fileName = fileName) + + moshi.adapter().fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + null + }, + ) + } + + /** Load list [V] values of asset file [fileName] */ + suspend inline fun loadList(fileName: String): List { + return runCatching(dispatchers.io) { + val json = assetReader.readJson(fileName = fileName) + + val type = Types.newParameterizedType(List::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + adapter.fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + emptyList() + }, + ) + } + + /** Load map [String] keys and [V] values of asset file [fileName] */ + suspend inline fun loadMap(fileName: String): Map { + return runCatching(dispatchers.io) { + val json = assetReader.readJson(fileName = fileName) + + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) + + adapter.fromJson(json) + } + .fold( + onSuccess = { parsedConfig -> + if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + parsedConfig.orEmpty() + }, + onFailure = { + Timber.e(it, "Failed to load config [$fileName] from assets") + emptyMap() + }, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt similarity index 79% rename from core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt rename to core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt index ce19ff204e..4a8678e96d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AndroidAssetReader.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.asset +package com.tangem.datasource.asset.reader import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext @@ -21,8 +21,7 @@ internal class AndroidAssetReader @Inject constructor( .use(BufferedReader::readText) } - override fun openFile(fileName: String): InputStream { - return context.assets - .open(fileName) + override fun openFile(file: String): InputStream { + return context.assets.open(file) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt rename to core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt index a3196c64aa..46c3edea4a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/reader/AssetReader.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.asset +package com.tangem.datasource.asset.reader import java.io.InputStream diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index d639cfbe6a..fe5ad1ad8e 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 @@ -99,8 +99,8 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { ), chiaFireAcademyApiKey = configValues.chiaFireAcademyApiKey, chiaTangemApiKey = configValues.chiaTangemApiKey, + polygonScanApiKey = configValues.polygonScanApiKey, ), - appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, sprinklr = configValues.sprinklr, walletConnectProjectId = configValues.walletConnectProjectId, @@ -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/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 25e8595d33..28e5b2b4d3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -8,7 +8,6 @@ data class Config( val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C", val mercuryoWidgetId: String = "", val mercuryoSecret: String = "", - val appsFlyerDevKey: String = "", val amplitudeApiKey: String = "", val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(), val isTopUpEnabled: Boolean = false, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 94987bf391..91ae26a0d4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass [REDACTED_AUTHOR] */ +// TODO remove class FeatureModel( val isTopUpEnabled: Boolean, val isCreatingTwinCardsAllowed: Boolean, @@ -30,7 +31,6 @@ class ConfigValueModel( @Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys, val blockcypherTokens: Set?, val infuraProjectId: String?, - val appsFlyer: AppsFlyer, val sprinklr: SprinklrConfig?, val tronGridApiKey: String, val amplitudeApiKey: String, @@ -41,6 +41,7 @@ class ConfigValueModel( val chiaTangemApiKey: String?, val devExpress: ExpressModel?, val express: ExpressModel?, + val polygonScanApiKey: String?, ) @JsonClass(generateAdapter = true) @@ -80,11 +81,6 @@ data class GetBlockToken( @Json(name = "rosetta") val rosetta: String?, ) -data class AppsFlyer( - val appsFlyerDevKey: String, - val appsFlyerAppID: String, -) - class ConfigModel( val features: FeatureModel?, val configValues: ConfigValueModel?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/ProviderModel.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/ProviderModel.kt new file mode 100644 index 0000000000..6bd7f5c07a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/ProviderModel.kt @@ -0,0 +1,32 @@ +package com.tangem.datasource.config.models + +import com.squareup.moshi.Json + +/** Config provider model */ +sealed class ProviderModel { + + /** + * Example, + * { + * "type": "public", + * "url": "https://example.com" + * } + */ + data class Public( + @Json(name = "url") val url: String, + ) : ProviderModel() + + /** + * Example, + * { + * "type": "private", + * "name": "nownodes" + * } + */ + data class Private( + @Json(name = "name") val name: String, + ) : ProviderModel() + + /** Unsupported type */ + data object UnsupportedType : ProviderModel() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AccountCreatorModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AccountCreatorModule.kt deleted file mode 100644 index 3b97ba24d9..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AccountCreatorModule.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.blockchain.common.AccountCreator -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.blockchain.DefaultAccountCreator -import com.tangem.lib.auth.AuthProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object AccountCreatorModule { - - @Provides - @Singleton - fun provideAccountCreator(authProvider: AuthProvider, tangemTechApi: TangemTechApi): AccountCreator { - return DefaultAccountCreator(authProvider, tangemTechApi) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt index 7df5502310..b39cb4f1a3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AssetModule.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.di -import com.tangem.datasource.asset.AndroidAssetReader -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.reader.AndroidAssetReader +import com.tangem.datasource.asset.reader.AssetReader import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/BlockchainDataStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/BlockchainDataStorageModule.kt deleted file mode 100644 index 3d285b3fbe..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/BlockchainDataStorageModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.blockchain.common.datastorage.BlockchainDataStorage -import com.tangem.datasource.local.blockchain.DefaultBlockchainDataStorage -import com.tangem.datasource.local.preferences.AppPreferencesStore -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object BlockchainDataStorageModule { - - @Provides - @Singleton - fun provideBlockchainDataStorage(appPreferencesStore: AppPreferencesStore): BlockchainDataStorage { - return DefaultBlockchainDataStorage(appPreferencesStore) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index c22d4433bf..3884c6c9a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -1,11 +1,13 @@ package com.tangem.datasource.di import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.BigDecimalAdapter import com.tangem.datasource.api.common.DateTimeAdapter import com.tangem.datasource.api.common.LocalDateAdapter +import com.tangem.datasource.config.models.ProviderModel import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,6 +23,12 @@ class MoshiModule { @NetworkMoshi fun provideNetworkMoshi(): Moshi { return Moshi.Builder() + .add( + PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type") + .withSubtype(ProviderModel.Public::class.java, "public") + .withSubtype(ProviderModel.Private::class.java, "private") + .withDefaultValue(ProviderModel.UnsupportedType), + ) .add(BigDecimalAdapter()) .add(LocalDateAdapter()) .add(DateTimeAdapter()) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index e52008bbc5..e687c394e9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -6,6 +6,8 @@ import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.TangemTechApiV2 +import com.tangem.datasource.api.tangemTech.TangemTechServiceApi import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.addLoggers @@ -19,6 +21,7 @@ import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory +import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @@ -55,53 +58,94 @@ class NetworkModule { @Provides @Singleton - fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(PROD_TANGEM_TECH_BASE_URL) - .client( - OkHttpClient.Builder() - .addHeaders( - CacheControlHeader, - // TODO("refactor header init") get auth data after biometric auth to avoid race condition - // AuthenticationHeader(authProvider), - ) - .addLoggers(context) - .build(), - ) - .build() - .create(TangemTechApi::class.java) + fun provideTangemTechApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechApi { + return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V1_TANGEM_TECH_BASE_URL) + } + + @Provides + @Singleton + fun provideTangemTechApiV2( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechApiV2 { + return provideTangemTechApiInternal(moshi, context, appVersionProvider, PROD_V2_TANGEM_TECH_BASE_URL) } @Provides @DevTangemApi @Singleton - fun provideTangemTechDevApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi { + fun provideTangemTechDevApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechApi { + return provideTangemTechApiInternal(moshi, context, appVersionProvider, DEV_V1_TANGEM_TECH_BASE_URL) + } + + @Provides + @Singleton + fun provideTangemTechServiceApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechServiceApi { + return provideTangemTechApiInternal( + moshi, + context, + appVersionProvider, + PROD_V1_TANGEM_TECH_BASE_URL, + timeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, + ) + } + + private inline fun provideTangemTechApiInternal( + moshi: Moshi, + context: Context, + appVersionProvider: AppVersionProvider, + baseUrl: String, + timeoutSeconds: Long? = null, + ): T { + val client = OkHttpClient.Builder() + .let { builder -> + if (timeoutSeconds != null) { + builder.callTimeout(timeoutSeconds, TimeUnit.SECONDS) + } else { + builder + } + } + .addHeaders( + CacheControlHeader, + AppVersionPlatformHeaders(appVersionProvider), + // TODO("refactor header init") get auth data after biometric auth to avoid race condition + // AuthenticationHeader(authProvider), + ) + .addLoggers(context) + .build() + return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) - .baseUrl(DEV_TANGEM_TECH_BASE_URL) - .client( - OkHttpClient.Builder() - .addHeaders( - CacheControlHeader, - // TODO("refactor header init") get auth data after biometric auth to avoid race condition - // AuthenticationHeader(authProvider), - ) - .addLoggers(context) - .build(), - ) + .baseUrl(baseUrl) + .client(client) .build() - .create(TangemTechApi::class.java) + .create(T::class.java) } private companion object { const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/" const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]" - const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" - const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" + const val DEV_V1_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" + + const val PROD_V1_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" + const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" + + const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L const val PAYMENTOLOGY_BASE_URL: String = "https://paymentologygate.oa.r.appspot.com/" const val API_ONE_INCH_TIMEOUT_MS = 5000L diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt index 229722e50d..bbd6ac04ad 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TestnetTokensStorageModule.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.di import com.squareup.moshi.Moshi import com.squareup.moshi.adapter -import com.tangem.datasource.asset.AssetReader +import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.local.testnet.DefaultTestnetTokensStorage import com.tangem.datasource.local.testnet.TestnetTokensStorage import dagger.Module diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/TangemDatabase.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/TangemDatabase.kt new file mode 100644 index 0000000000..7961908686 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/TangemDatabase.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.local.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.tangem.datasource.local.db.dao.CryptoCurrenciesAccountDao +import com.tangem.datasource.local.db.dao.CryptoCurrencyDao +import com.tangem.datasource.local.db.dao.UserWalletDao +import com.tangem.datasource.local.db.entity.CryptoCurrenciesAccountEntity +import com.tangem.datasource.local.db.entity.CryptoCurrencyEntity +import com.tangem.datasource.local.db.entity.UserWalletEntity +import com.tangem.datasource.local.db.utils.Converters + +@Database( + entities = [ + UserWalletEntity::class, + CryptoCurrenciesAccountEntity::class, + CryptoCurrencyEntity::class, + ], + version = 1, +) +@TypeConverters(Converters::class) +abstract class TangemDatabase : RoomDatabase() { + + abstract fun userWalletDao(): UserWalletDao + + abstract fun cryptoCurrencyDao(): CryptoCurrencyDao + + abstract fun cryptoCurrenciesAccountDao(): CryptoCurrenciesAccountDao +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrenciesAccountDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrenciesAccountDao.kt new file mode 100644 index 0000000000..92cbe0b2fc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrenciesAccountDao.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.local.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.tangem.datasource.local.db.entity.CryptoCurrenciesAccountEntity +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +@Dao +interface CryptoCurrenciesAccountDao { + + @Insert + suspend fun insert(account: CryptoCurrenciesAccountEntity) + + @Update + suspend fun update(account: CryptoCurrenciesAccountEntity) + + @Query("SELECT * FROM CryptoCurrenciesAccountEntity WHERE userWalletId = :userWalletId") + fun observeByUserWalletId(userWalletId: UserWalletId): Flow> + + @Query("SELECT * FROM CryptoCurrenciesAccountEntity WHERE userWalletId = :userWalletId") + suspend fun selectByUserWalletId(userWalletId: UserWalletId): List +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrencyDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrencyDao.kt new file mode 100644 index 0000000000..a7cbfec3cf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/CryptoCurrencyDao.kt @@ -0,0 +1,39 @@ +package com.tangem.datasource.local.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.tangem.datasource.local.db.entity.CryptoCurrencyEntity +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +@Dao +interface CryptoCurrencyDao { + + @Insert + suspend fun insert(currencies: List) + + @Query( + """ + SELECT * FROM CryptoCurrencyEntity + WHERE userWalletId = :userWalletId AND accountId = :accountId + """, + ) + suspend fun selectByAccountId(userWalletId: UserWalletId, accountId: Int): List + + @Query( + """ + SELECT * FROM CryptoCurrencyEntity + WHERE userWalletId = :userWalletId AND accountId = :accountId + """, + ) + fun observeByAccountId(userWalletId: UserWalletId, accountId: Int): Flow> + + @Query( + """ + SELECT COUNT(*) FROM CryptoCurrencyEntity + WHERE userWalletId = :userWalletId + """, + ) + suspend fun countByUserWalletId(userWalletId: UserWalletId): Int +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/UserWalletDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/UserWalletDao.kt new file mode 100644 index 0000000000..31bca725e8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/dao/UserWalletDao.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.local.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.tangem.datasource.local.db.entity.UserWalletEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface UserWalletDao { + + @Insert + suspend fun insert(vararg wallets: UserWalletEntity) + + @Update + suspend fun update(wallet: UserWalletEntity) + + @Query("SELECT * FROM UserWalletEntity") + suspend fun selectAll(): List + + @Query("SELECT * FROM UserWalletEntity") + fun observeAll(): Flow> + + @Query("SELECT * FROM UserWalletEntity WHERE id = :id") + suspend fun selectById(id: String): UserWalletEntity? + + @Query("SELECT * FROM UserWalletEntity WHERE id = :id") + fun observeById(id: String): Flow +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/di/TangemDatabaseModule.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/di/TangemDatabaseModule.kt new file mode 100644 index 0000000000..9b52c19cb5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/di/TangemDatabaseModule.kt @@ -0,0 +1,28 @@ +package com.tangem.datasource.local.db.di + +import android.content.Context +import androidx.room.Room +import com.tangem.datasource.local.db.TangemDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TangemDatabaseModule { + + private const val DATABASE_NAME = "tangem_database.db" + + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): TangemDatabase { + return Room.databaseBuilder( + context, + TangemDatabase::class.java, + DATABASE_NAME, + ).build() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrenciesAccountEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrenciesAccountEntity.kt new file mode 100644 index 0000000000..d9c940359a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrenciesAccountEntity.kt @@ -0,0 +1,30 @@ +package com.tangem.datasource.local.db.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = UserWalletEntity::class, + parentColumns = ["id"], + childColumns = ["userWalletId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(value = ["id"]), + Index(value = ["userWalletId"]), + ], +) +data class CryptoCurrenciesAccountEntity( + @PrimaryKey + val id: Int, + val userWalletId: String, + val title: String, + val currenciesCount: Int, + val isArchived: Boolean, + val ordinalNumber: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrencyEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrencyEntity.kt new file mode 100644 index 0000000000..5aab1dcf2f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/CryptoCurrencyEntity.kt @@ -0,0 +1,42 @@ +package com.tangem.datasource.local.db.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + foreignKeys = [ + ForeignKey( + entity = CryptoCurrenciesAccountEntity::class, + parentColumns = ["id"], + childColumns = ["accountId"], + onDelete = ForeignKey.CASCADE, + ), + ForeignKey( + entity = UserWalletEntity::class, + parentColumns = ["id"], + childColumns = ["userWalletId"], + onDelete = ForeignKey.CASCADE, + ), + ], + indices = [ + Index(value = ["currencyBackendId"]), + Index(value = ["networkId"]), + Index(value = ["accountId"]), + Index(value = ["userWalletId"]), + ], +) +data class CryptoCurrencyEntity( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + val currencyBackendId: String?, + val networkId: String, + val accountId: Int, + val userWalletId: String, + val name: String, + val symbol: String, + val decimals: Int, + val contractAddress: String?, + val derivationPath: String?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/UserWalletEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/UserWalletEntity.kt new file mode 100644 index 0000000000..9f07d4a325 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/entity/UserWalletEntity.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.local.db.entity + +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + indices = [ + Index(value = ["id"], unique = true), + Index(value = ["ordinalNumber"], unique = true), + ], +) +data class UserWalletEntity( + @PrimaryKey + val id: String, + val name: String, + val artworkUrl: String, + val isMultiCurrency: Boolean, + val hasBackupError: Boolean, + val cardsInWallet: Set, + val ordinalNumber: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/db/utils/Converters.kt b/core/datasource/src/main/java/com/tangem/datasource/local/db/utils/Converters.kt new file mode 100644 index 0000000000..47c939a20e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/db/utils/Converters.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.local.db.utils + +import androidx.room.TypeConverter +import com.tangem.domain.wallets.models.UserWalletId + +internal class Converters { + + @TypeConverter + fun setFromString(value: String): Set { + return value.split(",").toSet() + } + + @TypeConverter + fun setToString(set: Set): String { + return set.joinToString(",") + } + + @TypeConverter + fun userWalletIdFromString(value: String): UserWalletId { + return UserWalletId(value) + } + + @TypeConverter + fun userWalletIdToString(userWalletId: UserWalletId): String { + return userWalletId.stringValue + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 8a7c3c3dff..e069422752 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -5,9 +5,13 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_ import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_APPLICATION_STOPPED_KEY import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN /** @@ -19,6 +23,8 @@ object PreferencesKeys { val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") } + val SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY by lazy { booleanPreferencesKey("saveUserWalletShown") } + val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") } val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") } @@ -79,6 +85,14 @@ object PreferencesKeys { val SEND_TAP_HELP_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "sendTapHelpPreview") } + val WAS_APPLICATION_STOPPED_KEY by lazy { booleanPreferencesKey(name = "applicationStopped") } + + val SHOULD_OPEN_WELCOME_ON_RESUME_KEY by lazy { booleanPreferencesKey(name = "openWelcomeOnResume") } + + val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") } + + val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } + fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") } @@ -86,6 +100,7 @@ object PreferencesKeys { internal fun getTapPrefKeysToMigrate(): Set { return setOf( SAVE_USER_WALLETS_KEY, + SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, APP_LAUNCH_COUNT_KEY, SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY, FUNDS_FOUND_DATE_KEY, @@ -93,6 +108,9 @@ internal fun getTapPrefKeysToMigrate(): Set { USED_CARDS_INFO_KEY, WAS_TWINS_ONBOARDING_SHOWN, IS_TANGEM_TOS_ACCEPTED_KEY, + WAS_APPLICATION_STOPPED_KEY, + SHOULD_OPEN_WELCOME_ON_RESUME_KEY, + SHOULD_SAVE_ACCESS_CODES_KEY, ) .map(Preferences.Key<*>::name) .toSet() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/testnet/DefaultTestnetTokensStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/testnet/DefaultTestnetTokensStorage.kt index d4a3a066e1..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 244c40eadf..19da1d1f14 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.10.0" }, - { - "name": "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED", - "version": "5.8.0" - }, { "name": "LOCAL_USER_LOGS_ENABLED", "version": "5.11.0" @@ -26,5 +22,21 @@ { "name": "WC_SOLANA_TX_SIGN_ENABLED", "version": "5.11.0" + }, + { + "name": "TOKEN_LIST_LCE_ENABLED", + "version": "5.11.0" + }, + { + "name": "CARDANO_TOKENS_SUPPORT_ENABLED", + "version": "5.11.0" + }, + { + "name": "STAKING_ENABLED", + "version": "undefined" + }, + { + "name": "FULL_RESET_ENABLED", + "version": "5.12.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 c10f726779..23dd6a8a8c 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -4,16 +4,12 @@ Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. Обратиться в поддержку - Попробовать снова Эта функция недоступна в демонстрационном режиме Причина: %s Не могу отправить транзакцию Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. - Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки. - Ваши предложения отправлены - Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, или запросите поддержку. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. @@ -28,8 +24,6 @@ Тёмная Светлая Как в системе - При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства - Системная Тема Настройки приложения Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" @@ -57,10 +51,12 @@ Заводские настройки Тип безопасности Настройки карты - Ввиду особенностей сети Cardano при транзакции токена %1$s помимо комиссии сети будет списано %2$s - Для совершения транзакции %1$s, вам необходимо внести немного %2$s (%3$s), чтобы покрыть комиссию сети и минимальное значение ADA для отправки. - Недостаточно ADA для отправки токена - Вывод всего баланса ADA невозможен при наличии средств на токенах сети Cardano. Сначала выведите средства на ваших токенах. + Помимо сетевой комиссий, сеть Cardano взимает %1$s ADA при транзакции с токеном %2$s + Требования к транзакции Cardano + Чтобы совершить транзакцию %1$s, внесите некоторую сумму ADA для покрытия сетевой комиссии и минимального значения ADA (рекомендуется 5 ADA) + Недостаточно ADA для транзакции + Вы должны поддерживать некоторое количество ADA, поскольку у вас на балансе есть токены в сети Cardano + Недостаточно ADA Принять Доступ запрещен Применить @@ -81,7 +77,6 @@ Создать Удалить Отключено - Отключить Готово Включить Включено @@ -115,7 +110,6 @@ Отклонить Перезагрузить Переименовать - Повторить Сохранить изменения Искать Поиск токенов @@ -139,7 +133,6 @@ Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно - Предупреждение Да Адрес контракта скопирован! Доступные сети @@ -186,7 +179,6 @@ Скрывать балансы жестом переворота Эмитент Подписано - Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно. Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования @@ -241,6 +233,7 @@ Требуется разрешение Условиями использования Токены не найдены. Пожалуйста, попробуйте другой запрос + ID: %s ID транзакции скопирован Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. @@ -388,7 +381,9 @@ Сортировка токенов Список Выбрать из галереи - Нравится наше приложение? + Настройки + Вы не предоставили доступ к вашей камере + Доступ к камере запрещен %1$s (%2$s) в сети %3$s Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. Участвовать @@ -443,7 +438,7 @@ Сканировать Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. Приготовьте свою карту - Уже содержится в введенном адресе + Уже содержится во введенном адресе Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? Причина: %1$s\nКод: %2$s @@ -474,13 +469,10 @@ Всё Максимальная сумма Комиссия не превысит - Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. Допустим ввод только цифр - Сумма отправки будет уменьшена на %1$s (%2$s) для покрытия выбранного уровня комиссии Покрытие сетевой комиссии Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса Недостаточно средств - Оставить %s Аккаунт будет удален из блокчейна, если баланс упадет ниже экзистенциального депозита. Пожалуйста, оставьте %s на балансе. Экзистенциальный депозит Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. @@ -492,6 +484,7 @@ Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s. Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить. Сумма отправки не может быть менее %s + Оставить %s Уменьшить на %s Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции Возможны задержки по транзакции @@ -520,6 +513,9 @@ Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время Неверный адрес Транзакция отправлена + Забыть кошелек + Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. + Имя Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -540,7 +536,6 @@ Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств - Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии Подтвердить Текущая транзакция Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. @@ -558,11 +553,13 @@ Балансы показаны Отменить Выбранная операция в данный момент недоступна. Попробуйте позже. - В данный момент покупка монеты %s недоступна. Но мы работаем над её добавлением. - У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. - Обмен %s не доступен. Но мы работаем над его добавлением. - В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением. - Выберите адрес + В данный момент покупка монеты %s недоступна. Следите за нашими обновлениями. + У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. + У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. + В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. + Продажа средств станет доступной после завершения транзакции(-ий) в сети %s + Отправка средств станет доступной после завершения транзакции(-ий) в сети %s + В данный момент продажа %s недоступна. Следите за нашими обновлениями. Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. @@ -570,7 +567,7 @@ Скрыть токен %1$s токен в сети %%image%% %2$s Токен в сети %%image%% %1$s - Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. + Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети Невозможно скрыть %s Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии @@ -599,6 +596,7 @@ Вы уверены, что хотите удалить этот кошелек? Произошла ошибка, пожалуйста, отсканируйте свою карту для входа Этот кошелек уже был сохранен, вы можете добавить другой + Кошелек с именем %s уже существует Имя кошелька Переименование кошелька Разблокировать все @@ -641,6 +639,7 @@ Сеть %s Адрес скопирован в буфер обмена Нет соединения с интернетом + Настройки кошелька Tangem Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. @@ -675,9 +674,10 @@ Возможно, данная карта - образец или подделка Ошибка проверки подлинности Ассоциировать - Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%.4f %s + Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%1$s %2$s Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять Ассоциируете свой токен + Недостаточно %s. Пополните ваш аккаунт Hedera для ассоциации этого токена На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. Малое количество подписей Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. @@ -697,12 +697,10 @@ Карта уже подписывала транзакции Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше Нравится Tangem? + Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его Необходима плата за аренду сети %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. Недостаточно %1$s для оплаты комиссии сети - Отправка средств станет доступной после завершения транзакции(-ий) в сети %s - Отправка средств станет доступной после завершения транзакции %s - Транзакция в обработке Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку. Оповещение сети Солана Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b08722ddd9..99df15650b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,16 +4,12 @@ Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Request support - Try again This feature is disabled in Demo mode Reason: %s Can\'t send a transaction The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. Tokens in %1$s network are not supported by this card due to firmware limitation. - Thank you for your feedback. We will respond as soon as possible - Your suggestions were sent - Please try to tap the card exactly as shown in the animation or request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -29,8 +25,6 @@ Dark Light System default - If system is selected, the app will auto-adjust based on your device\'s system settings - System Theme App Settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings @@ -56,10 +50,12 @@ Reset to Factory Settings Security Mode Card Settings - Due to the peculiarities of the Cardano network, when transacting the %1$s token, in addition to the network commission, %2$s will be charged - To make a %1$s transaction, you must deposit some %2$s (%3$s) to cover the network fee and minimum ADA value - Insufficient ADA to token transfer - Withdrawal of the entire ADA balance is not possible if funds are available in Cardano network tokens. First, withdraw funds on your tokens. + In addition to network fee, the Cardano network charges %1$s ADA when transacting with the %2$s token + Cardano transaction requirements + To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value (5 ADA recommended) + Insufficient ADA for token transfer + You must maintain some ADA because you have some tokens on the Cardano blockchain + Not enough ADA Accept Access denied Apply @@ -80,7 +76,6 @@ Create Delete Disabled - Disconnect Done Enable Enabled @@ -114,7 +109,6 @@ Reject Reload Rename - Retry Save changes Search Search tokens @@ -138,7 +132,6 @@ I understand There was an error. Please try again. Unreachable - Warning Yes Contract address copied! Available networks @@ -185,7 +178,6 @@ Flip-to-Hide Balances Issuer Signed - If you forget the code you will lose access to your funds. Code recovery is not possible. Details Check your internet connection or switch to a different network Terms of Service @@ -387,7 +379,6 @@ Settings You have not given access to your camera Camera access denied - Enjoying our app? %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. Participate @@ -471,9 +462,7 @@ Max Maximum amount Max fee - The fee that will be charged for your transaction. You can set your own value. Numbers only for Destination Tag - Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level Network fee coverage Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance Total exceeds balance @@ -520,6 +509,9 @@ Invalid address %1$s (%2$s) Transaction sent + Forget wallet + This will remove the wallet from the application. The wallet itself can be added again. + Name Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -540,7 +532,6 @@ Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds - Sending amount will be reduced to cover the selected fee level Approve Current transaction The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. @@ -558,11 +549,13 @@ Balances shown Undo This operation is currently unavailable. Please try again later. - The purchase of the %s is currently unavailable. But we are working on adding it. - You do not have funds to send. Top up your account to be able to send funds from it. - %s swap is not available. But we are working on adding it. - Sell of the %s coin is currently unavailable. But we are working on adding it. - Choose address + Buying %s is not available at the moment. Please check our updates. + You do not have funds to sell. Top up your account to be able to sell funds from it. + You do not have funds to send. Top up your account to be able to send funds from it. + Swapping %s is not available at the moment. Please check our updates. + Selling funds will be available once the pending transaction(s) in network %s is complete + Sending funds will be available once the pending transaction(s) in network %s is complete + Selling %s is not available at the moment. Please check our updates. Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -570,7 +563,7 @@ Hide token %1$s token in %%image%% %2$s network Token in %%image%% %1$s network - The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. + The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list Unable to hide %s Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees @@ -599,6 +592,7 @@ Are you sure you want to delete this wallet? An error has occurred, please scan your card to log in This wallet has already been saved, you can add another one + The wallet with name %s already exists Wallet name Rename Wallet Unlock all @@ -641,6 +635,7 @@ %s network Address was copied to clipboard No internet connection + Wallet settings Tangem Use %s or scan a card to unlock access to your wallet Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. @@ -675,9 +670,10 @@ This card might be a production sample or counterfeit Authenticity check failed Associate - This token must be associated with your Hedera account before you can receive it. Association fee ~%.4f %s + This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s This token must be associated with your Hedera account before you can receive it Associate your token + Not enough %s. Top up your Hedera account to associate this token Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. @@ -695,12 +691,10 @@ Card has already signed transactions Your review keeps us motivated to make Tangem Wallet even better Enjoying Tangem? + You must associate your token before receiving tokens Network rent fee required %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. Insufficient %1$s to cover network fee - Sending funds will be available once the pending transaction(s) in network %s is complete - Sending funds will be available once the %s transaction is complete - Transaction pending The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction. Solana Network Alert Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3129c8917b..e7c5bfc4e7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { /** Project - Core */ implementation(projects.core.res) implementation(projects.core.utils) + implementation(projects.core.decompose) /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) diff --git a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt new file mode 100644 index 0000000000..62e19d4af6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui + +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.theme.AppThemeModeHolder + +interface UiDependencies { + + val hapticManager: HapticManager + + val appThemeModeHolder: AppThemeModeHolder +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt new file mode 100644 index 0000000000..2e6354a2e2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.clipboard + +import androidx.compose.runtime.Stable + +/** + * Ready to use clipboard manager. + * Does not require context to work + */ +@Stable +interface ClipboardManager { + + /** Copies to clipboard [text] with optional [label] */ + fun setText(label: String = "", text: String) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 1e2301844b..bb17fcf7b6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -12,6 +13,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.common.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme // region TextButton @@ -319,17 +321,10 @@ private fun PrimaryButtonSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PrimaryButtonPreview_Light() { - TangemTheme { - PrimaryButtonSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun PrimaryButtonPreview_Dark() { - TangemTheme(isDark = true) { +private fun PrimaryButtonPreview() { + TangemThemePreview { PrimaryButtonSample() } } @@ -383,17 +378,10 @@ private fun SecondaryButtonSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SecondaryButtonPreview_Light() { - TangemTheme { - SecondaryButtonSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SecondaryButtonPreview_Dark() { - TangemTheme(isDark = true) { +private fun SecondaryButtonPreview() { + TangemThemePreview { SecondaryButtonSample() } } @@ -414,19 +402,11 @@ private fun TextButtonSample() { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TextButtonPreview_LightTheme() { - TangemTheme { +private fun TextButtonPreview() { + TangemThemePreview { TextButtonSample() } } - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun TextButtonPreview_DarkTheme() { - TangemTheme(isDark = true) { - TextButtonSample() - } -} - // endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt b/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt deleted file mode 100644 index 5654a037e7..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt +++ /dev/null @@ -1,222 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.annotation.DrawableRes -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.res.ButtonColorType -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TextColorType -import com.tangem.core.ui.res.buttonColor -import com.tangem.core.ui.res.textColor - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = true, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryEndIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem_24, - enabled = false, - onClick = {}, - ) - } -} - -/** - * Primary button with an icon at the beginning of the layout - * - * @param modifier button modifier - * @param text button text - * @param iconResId button icon res id - * @param enabled controls the enabled state of the button - * @param onClick the lambda to be invoked when this button is pressed - * - * @see Figma component - */ -@Deprecated("Use PrimaryButtonIconRight instead") -@Composable -fun PrimaryStartIconButton( - text: String, - @DrawableRes iconResId: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, -) { - PrimaryButtonRow( - modifier = modifier, - enabled = enabled, - onClick = onClick, - content = { - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - ) - SpacerW8() - Text(text = text) - }, - ) -} - -/** - * Primary button with an icon at the end of the layout - * - * @param modifier button modifier - * @param text button text - * @param iconResId button icon res id - * @param enabled controls the enabled state of the button - * @param onClick the lambda to be invoked when this button is pressed - * - * @see Figma component - */ -@Deprecated("Use PrimaryButtonIconLeft instead") -@Composable -fun PrimaryEndIconButton( - text: String, - @DrawableRes iconResId: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, -) { - PrimaryButtonRow( - modifier = modifier, - enabled = enabled, - onClick = onClick, - content = { - Text(text = text) - SpacerW8() - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - ) - }, - ) -} - -@Composable -private fun PrimaryButtonRow( - enabled: Boolean, - onClick: () -> Unit, - content: @Composable (RowScope.() -> Unit), - modifier: Modifier = Modifier, -) { - Button( - onClick = onClick, - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size48), - enabled = enabled, - shape = RoundedCornerShape(TangemTheme.dimens.radius12), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.PRIMARY), - contentColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2), - disabledBackgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.DISABLED), - disabledContentColor = MaterialTheme.colors.textColor(type = TextColorType.DISABLED), - ), - content = content, - ) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt index 91dd9a2506..380c6bd97b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.valentinilk.shimmer.shimmer import kotlinx.collections.immutable.toImmutableList @@ -650,34 +652,19 @@ private fun CardsPreview() { } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Cards_InLightTheme() { - TangemTheme(isDark = false) { - CardsPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_InfoCardWithWarning_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_Cards() { + TangemThemePreview { CardsPreview() } } @Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Preview(widthDp = 328, heightDp = 48, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SimpleInfoCard_InLightTheme() { - TangemTheme(isDark = false) { - SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") - SmallInfoCardWithDisclaimer(startText = "Balance", endText = "0.4405434 BTC", disclaimer = "test") - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -private fun Preview_SimpleInfoCard_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_SimpleInfoCard() { + TangemThemePreview { SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") SmallInfoCardWithDisclaimer(startText = "Balance", endText = "0.4405434 BTC", disclaimer = "test") } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt index 8d10e7486c..71b0550226 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/CurrencyPlaceholderIcon.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding @@ -11,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -39,19 +41,11 @@ fun CurrencyPlaceholderIcon(id: String, modifier: Modifier = Modifier) { // region preview @Preview(showBackground = true, heightDp = 40, widthDp = 40) +@Preview(showBackground = true, heightDp = 40, widthDp = 40, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_CurrencyPlaceholderIcon_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_CurrencyPlaceholderIcon() { + TangemThemePreview { CurrencyPlaceholderIcon(id = "DAI") } } - -@Preview(showBackground = true, heightDp = 40, widthDp = 40) -@Composable -private fun Preview_CurrencyPlaceholderIcon_InDarkTheme() { - TangemTheme(isDark = true) { - CurrencyPlaceholderIcon(id = "DAI") - } -} - // endregion preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 658d6af9f6..18ca78091a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -26,6 +27,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.fields.SimpleDialogTextField +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -450,33 +452,19 @@ private fun BasicDialogPreview() { } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SimpleOkDialog_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_SimpleOkDialog() { + TangemThemePreview { SimpleOkDialogPreview() } } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_BasicDialog_InLightTheme() { - TangemTheme(isDark = false) { - BasicDialogPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_SimpleOkDialog_InDarkTheme() { - TangemTheme(isDark = true) { - SimpleOkDialogPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_BasicDialog_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_BasicDialog() { + TangemThemePreview { BasicDialogPreview() } } @@ -498,17 +486,10 @@ private fun WarningBasicDialogSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun WarningBasicDialogPreview_Light() { - TangemTheme { - WarningBasicDialogSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun WarningBasicDialogPreview_Dark() { - TangemTheme(isDark = true) { +private fun WarningBasicDialogPreview() { + TangemThemePreview { WarningBasicDialogSample() } } @@ -532,44 +513,19 @@ private fun TextInputDialogSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TextInputDialogPreview_Light() { - TangemTheme { +private fun TextInputDialogPreview() { + TangemThemePreview { TextInputDialogSample() } } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TextInputDialogPreview_Dark() { - TangemTheme(isDark = true) { - TextInputDialogSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SelectorDialogPreview_Light( - @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, -) { - TangemTheme(isDark = false) { - SelectorDialog( - title = param.title, - items = param.items, - selectedItemIndex = param.selectedItemIndex, - confirmButton = DialogButton(title = "Cancel", onClick = {}), - onSelect = {}, - onDismissDialog = {}, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun SelectorDialogPreview_Dark( - @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, -) { - TangemTheme(isDark = true) { +private fun SelectorDialogPreview(@PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams) { + TangemThemePreview { SelectorDialog( title = param.title, items = param.items, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt index a646f37f33..5e3e78a06a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.heightIn @@ -13,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=WSV3AxC6zV1y0CHF-1) @@ -44,17 +46,10 @@ fun Notifier( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TangemNotifierPreview_Light(@PreviewParameter(NotifierProvider::class) text: String) { - TangemTheme(isDark = false) { - Notifier(text = text) - } -} - -@Preview -@Composable -private fun TangemNotifierPreview_Dark(@PreviewParameter(NotifierProvider::class) text: String) { - TangemTheme(isDark = true) { +private fun TangemNotifierPreview(@PreviewParameter(NotifierProvider::class) text: String) { + TangemThemePreview { Notifier(text = text) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 311fe1aae8..ddb5122ee1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.infiniteRepeatable @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.valentinilk.shimmer.* @@ -90,37 +92,24 @@ private val TangemShimmerColors: List // region preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun ShimmersPreview() { - Column( - modifier = Modifier - .fillMaxWidth() - .background(TangemTheme.colors.background.primary), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), - ) { - RectangleShimmer( + TangemThemePreview { + Column( modifier = Modifier .fillMaxWidth() - .height(TangemTheme.dimens.size24), - ) - CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) + .background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), + ) { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size24), + ) + CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) + } } } - -@Preview(showBackground = true) -@Composable -private fun Shimmers_InLightTheme() { - TangemTheme(isDark = false) { - ShimmersPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Shimmers_InDarkTheme() { - TangemTheme(isDark = true) { - ShimmersPreview() - } -} - // endregion preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt index f8312aa659..055eaab6f7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState @@ -25,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -505,17 +507,10 @@ private fun OutlineTextFieldSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun OutlineTextFieldPreview_Light() { - TangemTheme { - OutlineTextFieldSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun OutlineTextFieldPreview_Dark() { - TangemTheme(isDark = true) { +private fun OutlineTextFieldPreview() { + TangemThemePreview { OutlineTextFieldSample() } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index fe2a681d08..cee6370513 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -13,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -179,42 +181,29 @@ private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @C // region Preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun WarningsPreview() { - Column(modifier = Modifier.fillMaxWidth()) { - WarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - ) - SpacerH32() - ClickableWarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - onClick = {}, - ) - SpacerH32() - RefreshableWarningCard( - title = "Exchange rate has expired", - description = "To access all the networks, you need to scan the card.", - onClick = {}, - ) + TangemThemePreview { + Column(modifier = Modifier.fillMaxWidth()) { + WarningCard( + title = "Exchange rate has expired", + description = "To access all the networks, you need to scan the card.", + ) + SpacerH32() + ClickableWarningCard( + title = "Exchange rate has expired", + description = "To access all the networks, you need to scan the card.", + onClick = {}, + ) + SpacerH32() + RefreshableWarningCard( + title = "Exchange rate has expired", + description = "To access all the networks, you need to scan the card.", + onClick = {}, + ) + } } } - -@Preview(showBackground = true) -@Composable -private fun Preview_Warning_InLightTheme() { - TangemTheme(isDark = false) { - WarningsPreview() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_Warning_InDarkTheme() { - TangemTheme(isDark = true) { - WarningsPreview() - } -} - // endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index 1b910ca269..685358db13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -15,6 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -71,9 +73,10 @@ fun AppBarWithAdditionalButtons( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AppBarWithAdditionalButtons_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_AppBarWithAdditionalButtons() { + TangemThemePreview { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -89,27 +92,10 @@ private fun Preview_AppBarWithAdditionalButtons_InLightTheme() { } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AppBarWithAdditionalButtons_InDarkTheme() { - TangemTheme(isDark = true) { - AppBarWithAdditionalButtons( - text = "Tangem", - startButton = AdditionalButton( - iconRes = R.drawable.ic_scan_24, - onIconClicked = {}, - ), - endButton = AdditionalButton( - iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = {}, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_AppBarWithOnlyStartButtons() { + TangemThemePreview { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -121,37 +107,10 @@ private fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AppBarWithOnlyStartButtons_InDarkTheme() { - TangemTheme(isDark = true) { - AppBarWithAdditionalButtons( - text = "Tangem", - startButton = AdditionalButton( - iconRes = R.drawable.ic_scan_24, - onIconClicked = {}, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun Preview_AppBarWithOnlyEndButtons_InLightTheme() { - TangemTheme(isDark = false) { - AppBarWithAdditionalButtons( - text = "Tangem", - endButton = AdditionalButton( - iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = {}, - ), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun Preview_AppBarWithOnlyEndButtons_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AppBarWithOnlyEndButtons() { + TangemThemePreview { AppBarWithAdditionalButtons( text = "Tangem", endButton = AdditionalButton( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index afb57565f8..80d3014615 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -15,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -67,17 +69,10 @@ fun AppBarWithBackButton( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewAppBarWithBackButtonInLightTheme() { - TangemTheme(isDark = false) { - AppBarWithBackButton(text = "Title", onBackClick = {}) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun PreviewAppBarWithBackButtonInDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewAppBarWithBackButton() { + TangemThemePreview { AppBarWithBackButton(text = "Title", onBackClick = {}) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index 405217433f..0006bdad9d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.clickable @@ -11,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @Composable @@ -53,22 +55,10 @@ fun AppBarWithBackButtonAndIcon( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewAppBarWithBackButtonAndIconInLightTheme() { - TangemTheme(isDark = false) { - AppBarWithBackButtonAndIcon( - text = "Title", - iconRes = R.drawable.ic_qrcode_scan_24, - onBackClick = {}, - onIconClick = {}, - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewAppBarWithBackButtonAndIcon() { + TangemThemePreview { AppBarWithBackButtonAndIcon( text = "Title", iconRes = R.drawable.ic_qrcode_scan_24, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt index c65bf02622..fae3598a38 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.appbar +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background @@ -17,6 +18,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -98,34 +100,10 @@ fun AppBarWithBackButtonAndIconContent( } @Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewAppBarWithBackButtonAndIconInLightTheme() { - TangemTheme(isDark = false) { - AppBarWithBackButtonAndIconContent( - text = "Title", - onBackClick = {}, - iconContent = { - Row { - Icon( - painter = painterResource(id = R.drawable.ic_qrcode_scan_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Icon( - painter = painterResource(id = R.drawable.ic_flash_on_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } - }, - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Composable -private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewAppBarWithBackButtonAndIcon() { + TangemThemePreview { AppBarWithBackButtonAndIconContent( text = "Title", subtitle = "Subtitle", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index 4dfd01d36d..ab6996b21d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -26,6 +26,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * App bar with close icon and search functionality @@ -228,7 +229,7 @@ private fun ExpandedSearchView( @Preview @Composable private fun CollapsedSearchViewPreview() { - TangemTheme { + TangemThemePreview { ExpandableSearchView( title = "Choose Token", onBackClick = {}, @@ -245,7 +246,7 @@ private fun CollapsedSearchViewPreview() { @Preview @Composable private fun ExpandedSearchViewPreview() { - TangemTheme { + TangemThemePreview { ExpandableSearchView( title = "Choose Token", onBackClick = {}, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt index f3d155f5a2..1f9dd3beca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.atoms +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable @@ -8,6 +9,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -52,17 +54,10 @@ private fun HandSample(modifier: Modifier = Modifier) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun HandPreview_Light() { - TangemTheme { - HandSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun HandPreview_Dark() { - TangemTheme(isDark = true) { +private fun HandPreview() { + TangemThemePreview { HandSample() } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt index 7433327856..6a851a2abf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/text/EllipsisText.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Constraints import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview sealed class TextEllipsis { @@ -201,7 +202,7 @@ private fun offsetEndEllipsisText( @Preview(widthDp = 200) @Composable private fun EllipsisTexPreview(@PreviewParameter(EllipsisTexPreviewParameterProvider::class) ellipsis: TextEllipsis) { - TangemTheme { + TangemThemePreview { EllipsisText( text = "11111111111111111111111111111111111111111111111111 END", ellipsis = ellipsis, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index 1057a43847..46317b571e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -1,6 +1,5 @@ package com.tangem.core.ui.components.bottomsheets.tokenreceive -import android.widget.Toast import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -9,10 +8,12 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalClipboardManager @@ -26,9 +27,11 @@ import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rememberQrPainters +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme +import kotlinx.coroutines.launch @Composable fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) { @@ -40,117 +43,88 @@ fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) { var selectedAddress by remember { mutableStateOf(content.addresses.first()) } - Column( - modifier = Modifier - .verticalScroll(state = rememberScrollState()) - .padding( - start = TangemTheme.dimens.spacing24, - top = TangemTheme.dimens.spacing24, - end = TangemTheme.dimens.spacing24, - bottom = TangemTheme.dimens.spacing16, - ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24), - ) { - QrCodeContent( - content = content, - onAddressChange = { selectedAddress = it }, - ) - Text( - text = stringResource(R.string.receive_bottom_sheet_warning_message_full, content.name), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + + val snackbarHostState = remember(::SnackbarHostState) + + ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) { + Column( + modifier = Modifier + .verticalScroll(state = rememberScrollState()) + .padding( + start = TangemTheme.dimens.spacing24, + top = TangemTheme.dimens.spacing24, + end = TangemTheme.dimens.spacing24, + bottom = TangemTheme.dimens.spacing16, + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24), ) { - val clipboardManager = LocalClipboardManager.current - val hapticFeedback = LocalHapticFeedback.current - val context = LocalContext.current - SecondaryButtonIconStart( - modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_copy), - iconResId = R.drawable.ic_copy_24, - onClick = { - content.onCopyClick.invoke() - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clipboardManager.setText(AnnotatedString(selectedAddress.value)) - Toast.makeText(context, R.string.wallet_notification_address_copied, Toast.LENGTH_SHORT).show() - }, - ) - SecondaryButtonIconStart( - modifier = Modifier.weight(1f), - text = stringResource(id = R.string.common_share), - iconResId = R.drawable.ic_share_24, - onClick = { - content.onShareClick.invoke() - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(selectedAddress.value) - }, - ) + QrCodeContent(content = content, onAddressChange = { selectedAddress = it }) + + DisclaimerText(text = content.name) + + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16)) { + CopyButton( + address = selectedAddress.value, + snackbarHostState = snackbarHostState, + onClick = content.onCopyClick, + modifier = Modifier.weight(1f), + ) + + ShareButton( + address = selectedAddress.value, + onClick = content.onShareClick, + modifier = Modifier.weight(1f), + ) + } } } } -@Suppress("LongMethod") +@Composable +private fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) { + Box { + content() + + CopiedTextSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing80), + ) + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) { val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value)) + val pagerState = rememberPagerState( initialPage = 0, initialPageOffsetFraction = 0f, - ) { - content.addresses.count() - } + pageCount = content.addresses::count, + ) LaunchedEffect(key1 = pagerState.currentPage) { onAddressChange.invoke(content.addresses[pagerState.currentPage]) } - HorizontalPager( - state = pagerState, - ) { currentPage -> - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), - ) { - Text( - text = stringResource( - R.string.receive_bottom_sheet_warning_message, - getName(content = content, index = pagerState.currentPage), - content.symbol, - content.network, - ), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h3, - ) - Image( - painter = qrCodes[currentPage], - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .size(TangemTheme.dimens.size248), - ) - Text( - text = content.addresses[currentPage].value, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.subtitle1, - ) - } + HorizontalPager(state = pagerState) { currentPage -> + QrCodePage( + content = content, + qrCodePainter = qrCodes[currentPage], + currentIndex = currentPage, + ) } if (pagerState.pageCount > 1) { val indicatorState = rememberLazyListState() val selectedColor = TangemTheme.colors.icon.primary1 val unselectedColor = TangemTheme.colors.icon.informative + LazyRow( - modifier = Modifier - .height(TangemTheme.dimens.size20), + modifier = Modifier.height(TangemTheme.dimens.size20), state = indicatorState, horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, @@ -158,17 +132,14 @@ private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChang repeat(pagerState.pageCount) { iteration -> item(key = iteration) { val color by animateColorAsState( - if (pagerState.currentPage == iteration) selectedColor else unselectedColor, + targetValue = if (pagerState.currentPage == iteration) selectedColor else unselectedColor, + label = "", ) + Box( modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing4, - top = TangemTheme.dimens.spacing6, - end = TangemTheme.dimens.spacing4, - bottom = TangemTheme.dimens.spacing6, - ) - .background(color, CircleShape) + .padding(horizontal = TangemTheme.dimens.spacing4, vertical = TangemTheme.dimens.spacing6) + .background(color = color, shape = CircleShape) .size(TangemTheme.dimens.size7), ) } @@ -177,6 +148,41 @@ private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChang } } +@Composable +private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Painter, currentIndex: Int) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + ) { + Text( + text = stringResource( + R.string.receive_bottom_sheet_warning_message, + getName(content = content, index = currentIndex), + content.symbol, + content.network, + ), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h3, + ) + + Image( + painter = qrCodePainter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(TangemTheme.dimens.size248), + ) + + Text( + text = content.addresses[currentIndex].value, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.subtitle1, + ) + } +} + @Composable private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String { return if (content.addresses.size < 2) { @@ -184,4 +190,61 @@ private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String } else { "${content.addresses[index].displayName.resolveReference()} ${content.name}" } +} + +@Composable +private fun DisclaimerText(text: String) { + Text( + text = stringResource(R.string.receive_bottom_sheet_warning_message_full, text), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2, + ) +} + +@Composable +private fun CopyButton( + address: String, + snackbarHostState: SnackbarHostState, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val clipboardManager = LocalClipboardManager.current + val coroutineScope = rememberCoroutineScope() + val resources = LocalContext.current.resources + + SecondaryButtonIconStart( + modifier = modifier, + text = stringResource(id = R.string.common_copy), + iconResId = R.drawable.ic_copy_24, + onClick = { + onClick() + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clipboardManager.setText(AnnotatedString(address)) + + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getString(R.string.wallet_notification_address_copied), + ) + } + }, + ) +} + +@Composable +private fun ShareButton(address: String, onClick: () -> Unit, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + SecondaryButtonIconStart( + modifier = modifier, + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + onClick() + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(address) + }, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index a6c3ee8402..715a630ed3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons +import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth @@ -15,6 +16,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -41,21 +43,12 @@ fun HorizontalActionChips( // region Preview @Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_HorizontalActionChips_Light( +private fun Preview_HorizontalActionChips( @PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips, ) { - TangemTheme(isDark = false) { - HorizontalActionChips(buttons = buttons.buttons) - } -} - -@Preview(widthDp = 360) -@Composable -private fun Preview_HorizontalActionChips_Dark( - @PreviewParameter(ActionButtonConfigProvider::class) buttons: HorizontalActionChips, -) { - TangemTheme(isDark = true) { + TangemThemePreview { HorizontalActionChips(buttons = buttons.buttons) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt index 60bc4a690f..15f640bdbf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons +import android.content.res.Configuration import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -14,6 +15,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -90,17 +92,10 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: } @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SmallButton_Light() { - TangemTheme(isDark = false) { - ButtonsSample() - } -} - -@Preview(showBackground = true) -@Composable -private fun Preview_SmallButton_Dark() { - TangemTheme(isDark = true) { +private fun Preview_SmallButton() { + TangemThemePreview { ButtonsSample() } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt index 6b9347623a..c93d92643e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.TextReference * @property text text * @property iconResId icon resource id * @property onClick lambda be invoked when action component is clicked + * @property onLongClick lambda be invoked when action component is long clicked * @property enabled enabled * @property dimContent determines whether the button content will be dimmed. This property will be ignored if [enabled] * is `false`. @@ -19,6 +20,7 @@ data class ActionButtonConfig( val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, + val onLongClick: (() -> TextReference?)? = null, val enabled: Boolean = true, val dimContent: Boolean = false, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 3d0b87e26a..60bc6e7e12 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -1,8 +1,11 @@ package com.tangem.core.ui.components.buttons.actions +import android.content.res.Configuration +import android.widget.Toast import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon @@ -13,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +27,7 @@ import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Rounded action button @@ -72,6 +77,7 @@ fun ActionButton( ) } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun Button( config: ActionButtonConfig, @@ -83,13 +89,24 @@ private fun Button( targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled, label = "Update background color", ) - + val context = LocalContext.current Row( modifier = modifier .heightIn(min = TangemTheme.dimens.size36) .clip(shape) .background(color = backgroundColor) - .clickable(enabled = config.enabled, onClick = config.onClick) + .combinedClickable( + enabled = config.enabled, + onClick = config.onClick, + onLongClick = { + val toastReference = config.onLongClick?.invoke() + toastReference?.let { + Toast + .makeText(context, toastReference.resolveReference(context.resources), Toast.LENGTH_SHORT) + .show() + } + }, + ) .padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24) .padding(vertical = TangemTheme.dimens.spacing8), horizontalArrangement = Arrangement.Center, @@ -133,33 +150,19 @@ private fun Button( } @Preview(group = "RoundedActionButton", showBackground = true) +@Preview(group = "RoundedActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_RoundedActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = false) { - RoundedActionButton(state) - } -} - -@Preview(group = "RoundedActionButton", showBackground = true) -@Composable -private fun Preview_RoundedActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = true) { +private fun Preview_RoundedActionButton(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { + TangemThemePreview { RoundedActionButton(state) } } @Preview(group = "ActionButton", showBackground = true) +@Preview(group = "ActionButton", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = false) { - ActionButton(state) - } -} - -@Preview(group = "ActionButton", showBackground = true) -@Composable -private fun Preview_ActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { - TangemTheme(isDark = true) { +private fun Preview_ActionButton(@PreviewParameter(ActionStateProvider::class) state: ActionButtonConfig) { + TangemThemePreview { ActionButton(state) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt index f3cbfca76d..0bfeb822a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.buttons.segmentedbutton +import android.content.res.Configuration import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring @@ -20,6 +21,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -102,29 +104,12 @@ inline fun SegmentedButtons( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SegmentedButtonsPreview_Light( +private fun SegmentedButtonsPreview( @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, ) { - TangemTheme { - SegmentedButtons( - config = config, - onClick = {}, - ) { - Text( - text = it.text, - modifier = Modifier.padding(TangemTheme.dimens.spacing16), - ) - } - } -} - -@Preview -@Composable -private fun SegmentedButtonsPreview_Dark( - @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, -) { - TangemTheme(isDark = true) { + TangemThemePreview { SegmentedButtons( config = config, onClick = {}, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index 7c185e50d6..c985d43cfc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.* import java.math.BigDecimal import java.text.DecimalFormat @@ -179,7 +180,7 @@ private fun AmountTextFieldPreview( @PreviewParameter(AmountTextFieldPreviewProvider::class) amount: AmountTextFieldPreviewData, ) { var text by remember { mutableStateOf(amount.value.orEmpty()) } - TangemTheme { + TangemThemePreview { AmountTextField( value = text, decimals = amount.decimals, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt index 23f26292ee..c4f107369a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/identicon/IdentIcon.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.icons.identicon +import android.content.res.Configuration import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size @@ -13,6 +14,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme private const val GAP_WIDTH = 1f @@ -62,9 +64,10 @@ fun IdentIcon(address: String, modifier: Modifier = Modifier) { //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun IdentIconPreview_Light() { - TangemTheme { +private fun IdentIconPreview() { + TangemThemePreview { IdentIcon( address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", modifier = Modifier @@ -72,17 +75,4 @@ private fun IdentIconPreview_Light() { ) } } - -@Preview -@Composable -private fun IdentIconPreview_Dark() { - TangemTheme(isDark = true) { - IdentIcon( - address = "0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359", - modifier = Modifier - .size(TangemTheme.dimens.size40), - ) - } -} - //endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt index 6310a11f5a..2bd8311866 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -123,40 +125,36 @@ private fun InputRowApproxItem( //region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowApproxPreview_Light() { - TangemTheme { - InputRowApprox( - leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title USD"), - leftSubtitle = TextReference.Str("Left subtitle USD"), - leftTitleEllipsisOffset = 3, - rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"), - rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"), - rightTitleEllipsisOffset = 3, - modifier = Modifier - .background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowApproxPreview_Dark() { - TangemTheme(isDark = true) { - InputRowApprox( - leftIcon = TokenIconState.Loading, - leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"), - leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"), - leftTitleEllipsisOffset = 3, - rightIcon = TokenIconState.Loading, - rightTitle = TextReference.Str("Right title USD"), - rightSubtitle = TextReference.Str("Right subtitle USD"), - rightTitleEllipsisOffset = 3, - modifier = Modifier - .background(TangemTheme.colors.background.action), - ) +private fun InputRowApproxPreview() { + TangemThemePreview { + Column { + InputRowApprox( + leftIcon = TokenIconState.Loading, + leftTitle = TextReference.Str("Left title USD"), + leftSubtitle = TextReference.Str("Left subtitle USD"), + leftTitleEllipsisOffset = 3, + rightIcon = TokenIconState.Loading, + rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"), + rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"), + rightTitleEllipsisOffset = 3, + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + InputRowApprox( + leftIcon = TokenIconState.Loading, + leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"), + leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"), + leftTitleEllipsisOffset = 3, + rightIcon = TokenIconState.Loading, + rightTitle = TextReference.Str("Right title USD"), + rightSubtitle = TextReference.Str("Right subtitle USD"), + rightTitleEllipsisOffset = 3, + modifier = Modifier + .background(TangemTheme.colors.background.action), + ) + } } } //endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index 620e3346cf..3f711de362 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -27,6 +28,7 @@ import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -154,30 +156,12 @@ private fun InnerIcon(imageUrl: String) { //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowBestRatePreview_Light( +private fun InputRowBestRatePreview( @PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData, ) { - TangemTheme { - InputRowBestRate( - imageUrl = "", - title = data.title, - titleExtra = data.titleExtra, - subtitle = data.subtitle, - showTag = data.showTag, - onIconClick = data.iconClick, - modifier = Modifier - .background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowBestRatePreview_Dark( - @PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowBestRate( imageUrl = "", title = data.title, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index e41ac02d17..4222d238c0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -19,6 +20,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -94,27 +96,12 @@ fun InputRowDefault( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowDefaultPreview_Light( +private fun InputRowDefaultPreview( @PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData, ) { - TangemTheme { - InputRowDefault( - title = TextReference.Str(data.title), - text = TextReference.Str(data.text), - iconRes = data.iconRes, - showDivider = data.showDivider, - modifier = Modifier.background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowDefaultPreview_Dark( - @PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowDefault( title = TextReference.Str(data.title), text = TextReference.Str(data.text), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt index f81372c3da..4d1490891f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnter.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -106,28 +108,12 @@ fun InputRowEnter( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowEnterPreview_Light( +private fun InputRowEnterPreview( @PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData, ) { - TangemTheme { - InputRowEnter( - title = TextReference.Str(data.title), - text = data.text, - iconRes = data.iconRes, - showDivider = data.showDivider, - onValueChange = {}, - modifier = Modifier.background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowEnterPreview_Dark( - @PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowEnter( title = TextReference.Str(data.title), text = data.text, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt index 25debb16dc..c9e01fd85f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfo.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -92,28 +94,12 @@ fun InputRowEnterInfo( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowEnterInfoPreview_Light( +private fun InputRowEnterInfoPreview( @PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData, ) { - TangemTheme { - InputRowEnterInfo( - title = data.title, - text = data.text, - info = data.info, - showDivider = data.showDivider, - onValueChange = {}, - modifier = Modifier.background(TangemTheme.colors.background.action), - ) - } -} - -@Preview -@Composable -private fun InputRowEnterInfoPreview_Dark( - @PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowEnterInfo( title = data.title, text = data.text, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt index 1754c3b8bc..35ba18ad8a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -120,31 +122,12 @@ fun InputRowImage( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowInputEnterInfoPreview_Light( +private fun InputRowInputEnterInfoPreview( @PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData, ) { - TangemTheme { - InputRowImage( - title = data.title, - modifier = Modifier.background(TangemTheme.colors.background.action), - subtitle = data.subtitle, - caption = data.caption, - tokenIconState = data.iconState, - iconRes = data.actionIconRes, - onIconClick = {}, - showNetworkIcon = false, - showDivider = data.showDivider, - ) - } -} - -@Preview -@Composable -private fun InputRowImagePreview_Dark( - @PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData, -) { - TangemTheme(isDark = true) { + TangemThemePreview { InputRowImage( title = data.title, modifier = Modifier.background(TangemTheme.colors.background.action), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index ccb48a3e6c..b1df3b3e40 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -23,6 +24,7 @@ import com.tangem.core.ui.components.inputrow.inner.PasteButton import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import kotlinx.coroutines.delay /** @@ -54,6 +56,7 @@ fun InputRowRecipient( isError: Boolean = false, showDivider: Boolean = false, isLoading: Boolean = false, + isValuePasted: Boolean = false, ) { val (titleText, color) = if (isError && error != null) { error to TangemTheme.colors.text.warning @@ -93,6 +96,7 @@ fun InputRowRecipient( placeholder = placeholder, onValueChange = onValueChange, singleLine = singleLine, + isValuePasted = isValuePasted, modifier = Modifier .padding(start = TangemTheme.dimens.spacing12) .weight(1f) @@ -152,11 +156,12 @@ private fun RowScope.InputIcon(isLoading: Boolean, value: String) { //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowRecipientPreview_Light( +private fun InputRowRecipientPreview( @PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData, ) { - TangemTheme { + TangemThemePreview { InputRowRecipient( value = value.value, title = TextReference.Res(R.string.send_recipient), @@ -172,27 +177,6 @@ private fun InputRowRecipientPreview_Light( } } -@Preview -@Composable -private fun InputRowRecipientPreview_Dark( - @PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData, -) { - TangemTheme(isDark = true) { - InputRowRecipient( - value = value.value, - title = TextReference.Res(R.string.send_recipient), - placeholder = TextReference.Res(R.string.send_optional_field), - error = TextReference.Str("Error"), - isLoading = value.isLoading, - isError = value.isError, - showDivider = true, - onValueChange = {}, - onPasteClick = {}, - modifier = Modifier.background(TangemTheme.colors.background.primary), - ) - } -} - private data class InputRowRecipientPreviewData( val value: String, val isError: Boolean, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt index 8ae6b1b386..94f75c6a56 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipientDefault.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.inputrow +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,6 +16,7 @@ import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -79,9 +81,10 @@ fun InputRowRecipientDefault( //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun InputRowRecipientPreview_Light() { - TangemTheme { +private fun InputRowRecipientPreview() { + TangemThemePreview { InputRowRecipientDefault( value = "0x391316d97a07027a0702c8A002c8A0C25d8470", title = TextReference.Res(R.string.send_recipient), @@ -90,17 +93,4 @@ private fun InputRowRecipientPreview_Light() { ) } } - -@Preview -@Composable -private fun InputRowRecipientPreview_Dark() { - TangemTheme(isDark = true) { - InputRowRecipientDefault( - value = "0x391316d97a07027a0702c8A002c8A0C25d8470", - title = TextReference.Res(R.string.send_recipient), - showDivider = false, - modifier = Modifier.background(TangemTheme.colors.background.primary), - ) - } -} //endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index 653cd250e6..af8a6761b3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION /** @@ -100,7 +101,7 @@ fun CrossIcon(onClick: (String) -> Unit, modifier: Modifier = Modifier) { @Composable private fun PasteButtonPreview() { var isVisible by remember { mutableStateOf(true) } - TangemTheme { + TangemThemePreview { PasteButton( isPasteButtonVisible = isVisible, onClick = { isVisible = !isVisible }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 873d6dbcb2..606416093a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.marketprice +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -19,6 +20,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter @@ -203,23 +205,13 @@ private fun QuoteTimeStatus() { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_MarketPriceBlock_Light( +private fun Preview_MarketPriceBlock( @PreviewParameter(WalletMarketPriceBlockStateProvider::class) state: MarketPriceBlockState, ) { - TangemTheme(isDark = false) { - MarketPriceBlock(state = state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun Preview_MarketPriceBlock_Dark( - @PreviewParameter(WalletMarketPriceBlockStateProvider::class) - state: MarketPriceBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { MarketPriceBlock(state = state) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 4a71c9c99c..c572bb3f83 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.notifications +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image @@ -29,6 +30,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState @@ -282,22 +284,13 @@ private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Mod } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Notification_Light( +private fun Preview_Notification( @PreviewParameter(NotificationConfigProvider::class) config: NotificationConfig, ) { - TangemTheme(isDark = false) { - Notification(config) - } -} - -@Preview -@Composable -private fun Preview_Notification_Dark( - @PreviewParameter(NotificationConfigProvider::class) config: NotificationConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { Notification(config) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 0c48f4b767..076c60da41 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.notifications +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -32,6 +33,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.TangemColorPalette.Dark6 import com.tangem.core.ui.res.TangemColorPalette.Light4 +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -153,21 +155,12 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = //region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun NotificationWithBackgroundPreview_Light( +private fun NotificationWithBackgroundPreview( @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, ) { - TangemTheme { - NotificationWithBackground(config = config) - } -} - -@Preview -@Composable -private fun NotificationWithBackgroundPreview_Dark( - @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { NotificationWithBackground(config = config) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt index 0c2a785acc..36c44d81b3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ActionRow.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH28 import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Simple clickable action row, without input and icon @@ -66,7 +67,7 @@ fun SimpleActionRow(title: String, description: String, modifier: Modifier = Mod @Composable private fun SimpleActionRowPreview() { Column { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { SimpleActionRow( title = "Title", description = "Description", @@ -75,7 +76,7 @@ private fun SimpleActionRowPreview() { SpacerH28() - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { SimpleActionRow( title = "Title", description = "Description", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index 3802d115d4..c1e06e7270 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.rows +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.animateColorAsState @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @Composable @@ -140,25 +142,10 @@ private fun RowScope.SelectorValueContent( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SelectorRowItemPreview_Light() { - TangemTheme { - SelectorRowItem( - titleRes = R.string.common_fee_selector_option_slow, - iconRes = R.drawable.ic_tortoise_24, - preDot = TextReference.Str("1000 ETH"), - postDot = TextReference.Str("1000 $"), - ellipsizeOffset = 4, - isSelected = true, - onSelect = { }, - ) - } -} - -@Preview -@Composable -private fun SelectorRowItemPreview_Dark() { - TangemTheme(isDark = true) { +private fun SelectorRowItemPreview() { + TangemThemePreview { SelectorRowItem( titleRes = R.string.common_fee_selector_option_slow, iconRes = R.drawable.ic_tortoise_24, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt new file mode 100644 index 0000000000..35cc8f6903 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt @@ -0,0 +1,114 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Snackbar to inform the user about copying text to the clipboard + * + * @param message message + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbar(message: TextReference, modifier: Modifier = Modifier) { + BaseSnackbar(message = message, modifier = modifier) +} + +/** + * Snackbar to inform the user about copying text to the clipboard. + * + * @param snackbarData this is needed to better support Material3.SnackbarHost, but only supports the message field + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbar(snackbarData: SnackbarData, modifier: Modifier = Modifier) { + BaseSnackbar(message = stringReference(snackbarData.visuals.message), modifier = modifier) +} + +@Composable +private fun BaseSnackbar(message: TextReference, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(color = TangemTheme.colors.icon.secondary, shape = TangemTheme.shapes.roundedCorners8) + .heightIn(min = TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing14, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + MarkIcon() + + MessageText(text = message, modifier = Modifier.weight(weight = 1f, fill = false)) + } +} + +@Composable +private fun MarkIcon() { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size20), + tint = TangemTheme.colors.icon.accent, + ) +} + +@Composable +private fun MessageText(text: TextReference, modifier: Modifier = Modifier) { + Text( + text = text.resolveReference(), + modifier = modifier, + color = TangemTheme.colors.text.disabled, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) +} + +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_CopiedTextSnackbar( + @PreviewParameter(CopiedTextSnackbarDataProvider::class) message: TextReference, +) { + TangemTheme(isDark = false) { + CopiedTextSnackbar(message = message) + } +} + +private class CopiedTextSnackbarDataProvider : CollectionPreviewParameterProvider( + collection = listOf( + stringReference(value = "Copied!"), + stringReference(value = "Contract address copied!"), + stringReference(value = "Coooooooooooontract addreeeeeeeeeeeeeeeess coooooooooooooooopied!"), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt new file mode 100644 index 0000000000..3cff3dd5c0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbarHost.kt @@ -0,0 +1,25 @@ +package com.tangem.core.ui.components.snackbar + +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * SnackbarHost to inform the user about copying text to the clipboard + * Based on Material3 component. It's best way to show [CopiedTextSnackbar]. + * + * @param hostState snackbar host state + * @param modifier modifier + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) { + SnackbarHost(hostState = hostState, modifier = modifier) { + CopiedTextSnackbar(snackbarData = it) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt new file mode 100644 index 0000000000..70cacc3a82 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbar.kt @@ -0,0 +1,84 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Tangem snackbar. + * Based on Material3 component. It can be presented as one line or multi lines snackbar – depends on text length. + * + * @param data snackbar data + * @param modifier modifier + * @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false) + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun TangemSnackbar(data: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { + Snackbar( + modifier = modifier, + action = { + ActionButton(label = data.visuals.actionLabel, onClick = data::performAction) + }, + actionOnNewLine = actionOnNewLine, + shape = TangemTheme.shapes.roundedCorners8, + containerColor = TangemTheme.colors.icon.secondary, + ) { + MessageText(text = data.visuals.message) + } +} + +@Composable +private fun ActionButton(label: String?, onClick: () -> Unit) { + if (!label.isNullOrBlank()) { + TextButton( + onClick = onClick, + colors = ButtonDefaults.textButtonColors( + contentColor = TangemTheme.colors.text.primary2, + ), + content = { + Text( + text = label, + maxLines = 1, + style = TangemTheme.typography.button, + ) + }, + ) + } +} + +@Composable +private fun MessageText(text: String) { + Text( + text = text, + color = TangemTheme.colors.text.disabled, + textAlign = TextAlign.Start, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.body2, + ) +} + +/** + * IMPORTANT! + * Preview doesn't work correctly, check on device or start [TangemSnackbarHost]'s preview in interactive mode * + */ +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) { + TangemThemePreview { + TangemSnackbar(data = model.snackbarData, actionOnNewLine = model.actionOnNewLine) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt new file mode 100644 index 0000000000..d1cd229f6d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarHost.kt @@ -0,0 +1,48 @@ +package com.tangem.core.ui.components.snackbar + +import android.content.res.Configuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Tangem snackbar host. + * Based on Material3 component. It's best way to show [TangemSnackbar]. + * + * @param hostState snackbar host state + * @param modifier modifier + * @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false) + * + * @see Figma + * +[REDACTED_AUTHOR] + */ +@Composable +fun TangemSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) { + SnackbarHost(hostState = hostState, modifier = modifier) { data -> + TangemSnackbar(data = data, actionOnNewLine = actionOnNewLine) + } +} + +@Preview(widthDp = 344, showBackground = true, fontScale = 1f) +@Preview(widthDp = 344, showBackground = true, fontScale = 1f, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 344, showBackground = true, fontScale = 2f) +@Composable +private fun Preview_TangemSnackbar(@PreviewParameter(TangemSnackbarModelProvider::class) model: TangemSnackbarModel) { + TangemThemePreview { + val snackbarHostState = remember(::SnackbarHostState) + + TangemSnackbarHost(hostState = snackbarHostState, actionOnNewLine = model.actionOnNewLine) + + LaunchedEffect(key1 = null) { + snackbarHostState.showSnackbar(visuals = model.snackbarData.visuals) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt new file mode 100644 index 0000000000..e2bdbf3ecf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/TangemSnackbarModelProvider.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components.snackbar + +import androidx.compose.material3.SnackbarData +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarVisuals +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider + +internal data class TangemSnackbarModel(val snackbarData: SnackbarData, val actionOnNewLine: Boolean) + +internal class TangemSnackbarModelProvider : CollectionPreviewParameterProvider( + listOf( + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Button", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Very loooooooooooong single-line description.", + actionLabel = "Button", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Very looooooong button name", + actionOnNewLine = false, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Button", + actionOnNewLine = true, + ), + createTangemSnackbarModel( + message = "Very loooooooooooong single-line description.", + actionLabel = "Button", + actionOnNewLine = true, + ), + createTangemSnackbarModel( + message = "Single-line description.", + actionLabel = "Very looooooong button name", + actionOnNewLine = true, + ), + ), +) { + + companion object { + + fun createTangemSnackbarModel( + message: String, + actionLabel: String, + actionOnNewLine: Boolean, + ): TangemSnackbarModel { + return TangemSnackbarModel( + snackbarData = createSnackbarData(message, actionLabel), + actionOnNewLine = actionOnNewLine, + ) + } + + private fun createSnackbarData(message: String, actionLabel: String): SnackbarData { + return object : SnackbarData { + + override val visuals: SnackbarVisuals + get() = object : SnackbarVisuals { + override val message: String = message + override val actionLabel: String = actionLabel + override val duration: SnackbarDuration = SnackbarDuration.Short // Never-mind + override val withDismissAction: Boolean = false // Never-mind + } + + override fun dismiss() = Unit + override fun performAction() = Unit + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 271e85f311..3c76514a21 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -32,6 +33,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import java.util.UUID @@ -318,21 +320,10 @@ private fun LockedContent(modifier: Modifier = Modifier) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TransactionItem_LightTheme( - @PreviewParameter(TransactionItemStateProvider::class) state: TransactionState, -) { - TangemTheme(isDark = false) { - Transaction(state = state, isBalanceHidden = false) - } -} - -@Preview -@Composable -private fun Preview_TransactionItem_DarkTheme( - @PreviewParameter(TransactionItemStateProvider::class) state: TransactionState, -) { - TangemTheme(isDark = true) { +private fun Preview_TransactionItem(@PreviewParameter(TransactionItemStateProvider::class) state: TransactionState) { + TangemThemePreview { Transaction(state = state, isBalanceHidden = false) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt index 62c98e0361..aea7aaa822 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -15,6 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat @@ -61,23 +63,10 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie // region Previews @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TransactionDoneTitlePreview_Light() { - TangemTheme { - TransactionDoneTitle( - titleRes = R.string.sent_transaction_sent_title, - date = 0, - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16), - ) - } -} - -@Preview -@Composable -private fun TransactionDoneTitlePreview_Dark() { - TangemTheme(isDark = true) { +private fun TransactionDoneTitlePreview() { + TangemThemePreview { TransactionDoneTitle( titleRes = R.string.sent_transaction_sent_title, date = 0, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt index b2b02d9e1a..e0298ceaed 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryGroupTitle.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -11,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import java.util.UUID @@ -42,19 +44,10 @@ internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TransactionsBlockGroupTitle_Light() { - TangemTheme(isDark = false) { - TxHistoryGroupTitle( - config = TxHistoryItemState.GroupTitle(title = "Today", itemKey = UUID.randomUUID().toString()), - ) - } -} - -@Preview -@Composable -private fun Preview_TransactionsBlockGroupTitle_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TransactionsBlockGroupTitle() { + TangemThemePreview { TxHistoryGroupTitle( config = TxHistoryItemState.GroupTitle(title = "Today", itemKey = UUID.randomUUID().toString()), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt index 352c19d181..3a7f89969c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TxHistoryTitle.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -12,6 +13,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -59,17 +61,10 @@ internal fun TxHistoryTitle(onExploreClick: () -> Unit, modifier: Modifier = Mod } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TransactionsBlockTitle_Light() { - TangemTheme(isDark = false) { - TxHistoryTitle(onExploreClick = {}) - } -} - -@Preview -@Composable -private fun Preview_TransactionsBlockTitle_Dark() { - TangemTheme(isDark = true) { +private fun Preview_TransactionsBlockTitle() { + TangemThemePreview { TxHistoryTitle(onExploreClick = {}) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt index c2b7368924..a6d96d536d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.components.transactions.empty +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme /** @@ -87,20 +89,11 @@ private fun PairButtons(state: EmptyTransactionsBlockState.ButtonsState.PairButt @Composable @Preview(widthDp = 360, showBackground = true) -private fun EmptyTransactionBlock_Light( +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun EmptyTransactionBlockPreview( @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, ) { - TangemTheme { - EmptyTransactionBlock(state = state) - } -} - -@Composable -@Preview(widthDp = 360, showBackground = true) -private fun EmptyTransactionBlock_Dark( - @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { EmptyTransactionBlock(state = state) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 1a95df0142..e6e331c589 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -67,6 +67,7 @@ fun getActiveIconRes(blockchainId: String): Int { "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 "base" -> R.drawable.img_base_22 + "joystream" -> R.drawable.img_joystream_22 else -> R.drawable.ic_alert_24 } } @@ -135,6 +136,7 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 "base" -> R.drawable.img_base_22 + "joystream" -> R.drawable.img_joystream_22 else -> R.drawable.ic_alert_24 } } @@ -200,6 +202,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "taraxa" -> R.drawable.img_taraxa_22 "radiant" -> R.drawable.img_radiant_22 "base" -> R.drawable.img_base_22 + "joystream" -> R.drawable.img_joystream_22 else -> R.drawable.ic_alert_24 } } @@ -268,6 +271,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 "base", "base/test" -> R.drawable.ic_base_22 + "joystream" -> R.drawable.ic_joystream_22 else -> R.drawable.ic_alert_24 } } @@ -336,6 +340,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 "radiant" -> R.drawable.ic_radiant_22 "base", "base/test" -> R.drawable.ic_base_22 + "joystream" -> R.drawable.ic_joystream_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt new file mode 100644 index 0000000000..b93c08bd26 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/HapticManager.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.haptic + +import androidx.compose.runtime.Stable + +@Stable +interface HapticManager { + + fun vibrateShort() + + fun vibrateMeduim() + + fun vibrateLong() +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt new file mode 100644 index 0000000000..876ee7a05d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.haptic + +object MockHapticManager : HapticManager { + + override fun vibrateShort() { + /** Intentionnaly do nothing */ + } + + override fun vibrateMeduim() { + /** Intentionnaly do nothing */ + } + + override fun vibrateLong() { + /** Intentionnaly do nothing */ + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessage.kt new file mode 100644 index 0000000000..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/res/ButtonColorType.kt b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColorType.kt deleted file mode 100644 index 2e45dc7577..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColorType.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.core.ui.res - -import androidx.compose.material.Colors -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.res.TangemColorPalette.Dark5 -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.DarkGreen -import com.tangem.core.ui.res.TangemColorPalette.Light1 -import com.tangem.core.ui.res.TangemColorPalette.Light2 -import com.tangem.core.ui.res.TangemColorPalette.MagicMint -import com.tangem.core.ui.res.TangemColorPalette.Meadow - -@Deprecated("Use TangemTheme.colors") -enum class ButtonColorType(val lightColor: Color, val darkColor: Color) { - PRIMARY(lightColor = Dark6, darkColor = Light1), - SECONDARY(lightColor = Light2, darkColor = Dark5), - DISABLED(lightColor = Light2, darkColor = Dark6), - POSITIVE(lightColor = Meadow, darkColor = Meadow), - POSITIVE_DISABLED(lightColor = MagicMint, darkColor = DarkGreen), -} - -@Composable -fun Colors.buttonColor(type: ButtonColorType): Color = if (IS_SYSTEM_IN_DARK_THEME) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt b/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt deleted file mode 100644 index 42b0e53057..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.core.ui.res - -import androidx.compose.material.Colors -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.res.TangemColorPalette.Amaranth -import com.tangem.core.ui.res.TangemColorPalette.Azure -import com.tangem.core.ui.res.TangemColorPalette.Black -import com.tangem.core.ui.res.TangemColorPalette.Dark1 -import com.tangem.core.ui.res.TangemColorPalette.Dark2 -import com.tangem.core.ui.res.TangemColorPalette.Dark4 -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.Light4 -import com.tangem.core.ui.res.TangemColorPalette.Light5 -import com.tangem.core.ui.res.TangemColorPalette.Mustard -import com.tangem.core.ui.res.TangemColorPalette.Tangerine -import com.tangem.core.ui.res.TangemColorPalette.White - -@Deprecated("Use TangemTheme.colors") -enum class IconColorType(val lightColor: Color, val darkColor: Color) { - PRIMARY1(lightColor = Black, darkColor = White), - PRIMARY2(lightColor = White, darkColor = Dark6), - SECONDARY(lightColor = Dark2, darkColor = Dark1), - INFORMATIVE(lightColor = Light5, darkColor = Dark2), - INACTIVE(lightColor = Light4, darkColor = Dark4), - ACCENT(lightColor = Azure, darkColor = Azure), - WARNING(lightColor = Amaranth, darkColor = Amaranth), - ATTENTION(lightColor = Tangerine, darkColor = Mustard), -} - -@Composable -fun Colors.iconColor(type: IconColorType): Color = if (IS_SYSTEM_IN_DARK_THEME) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 79a2e16273..447750132a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -131,6 +131,7 @@ data class TangemDimens internal constructor( val spacing92: Dp = 92.dp, val spacing94: Dp = 94.dp, val spacing96: Dp = 96.dp, + val spacing108: Dp = 108.dp, val spacing154: Dp = 154.dp, // endregion Spacing ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 78d91538e0..4f3287a86d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -4,6 +4,8 @@ import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle import androidx.compose.runtime.* +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.haptic.MockHapticManager // TODO: use isSystemInDarkTheme() for automatic color detection internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false @@ -13,6 +15,7 @@ fun TangemTheme( isDark: Boolean = false, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, + hapticManager: HapticManager = MockHapticManager, content: @Composable () -> Unit, ) { val themeColors = if (isDark) darkThemeColors() else lightThemeColors() @@ -29,6 +32,7 @@ fun TangemTheme( LocalTangemDimens provides dimens, LocalTangemShapes provides shapes, LocalIsInDarkTheme provides isDark, + LocalHapticManager provides hapticManager, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -196,4 +200,8 @@ private val LocalTangemShapes = staticCompositionLocalOf { error("No TangemShapes provided") } -val LocalIsInDarkTheme = staticCompositionLocalOf { false } \ No newline at end of file +val LocalIsInDarkTheme = staticCompositionLocalOf { false } + +val LocalHapticManager = staticCompositionLocalOf { + error("No HapticManager provided") +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt new file mode 100644 index 0000000000..2a9e0caf65 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -0,0 +1,21 @@ +package com.tangem.core.ui.res + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable + +@Composable +fun TangemThemePreview( + isDark: Boolean? = null, + typography: TangemTypography = TangemTheme.typography, + dimens: TangemDimens = TangemTheme.dimens, + content: @Composable () -> Unit, +) { + val isDarkTheme = isDark ?: isSystemInDarkTheme() + + TangemTheme( + isDark = isDarkTheme, + typography = typography, + dimens = dimens, + content = content, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TextColorType.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TextColorType.kt deleted file mode 100644 index b093f4f7af..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TextColorType.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.core.ui.res - -import androidx.compose.material.Colors -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.res.TangemColorPalette.Amaranth -import com.tangem.core.ui.res.TangemColorPalette.Dark1 -import com.tangem.core.ui.res.TangemColorPalette.Dark2 -import com.tangem.core.ui.res.TangemColorPalette.Dark3 -import com.tangem.core.ui.res.TangemColorPalette.Dark6 -import com.tangem.core.ui.res.TangemColorPalette.Light4 -import com.tangem.core.ui.res.TangemColorPalette.Light5 -import com.tangem.core.ui.res.TangemColorPalette.Meadow -import com.tangem.core.ui.res.TangemColorPalette.Mustard -import com.tangem.core.ui.res.TangemColorPalette.Tangerine -import com.tangem.core.ui.res.TangemColorPalette.White - -@Deprecated("Use TangemTheme.colors") -enum class TextColorType(val lightColor: Color, val darkColor: Color) { - PRIMARY1(lightColor = Dark6, darkColor = White), - PRIMARY2(lightColor = White, darkColor = Dark6), - SECONDARY(lightColor = Dark2, darkColor = Light5), - TERTIARY(lightColor = Dark1, darkColor = Dark1), - DISABLED(lightColor = Light4, darkColor = Dark3), - ACCENT(lightColor = Meadow, darkColor = Meadow), - WARNING(lightColor = Amaranth, darkColor = Amaranth), - ATTENTION(lightColor = Tangerine, darkColor = Mustard), - CONSTANT_WHITE(lightColor = White, darkColor = White), -} - -@Composable -fun Colors.textColor(type: TextColorType): Color = if (IS_SYSTEM_IN_DARK_THEME) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index f2eade80ca..4c2faae280 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.domain.apptheme.model.AppThemeMode /** @@ -22,9 +22,9 @@ import com.tangem.domain.apptheme.model.AppThemeMode internal interface ComposeScreen { /** - * The holder for managing the current application theme mode. + * The holder for ui dependencies. */ - val appThemeModeHolder: AppThemeModeHolder + val uiDependencies: UiDependencies /** * The screen modifier. @@ -53,9 +53,12 @@ internal interface ComposeScreen { internal fun ComposeScreen.createComposeView(context: Context): ComposeView { return ComposeView(context).apply { setContent { - val appThemeMode by appThemeModeHolder.appThemeMode + val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode - TangemTheme(isDark = shouldUseDarkTheme(appThemeMode)) { + TangemTheme( + isDark = shouldUseDarkTheme(appThemeMode), + hapticManager = uiDependencies.hapticManager, + ) { ScreenContent(modifier = screenModifier) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt new file mode 100644 index 0000000000..8548896b31 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.test + +object TestTags { + const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER" + const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" + const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" + + const val WALLET_SCREEN = "WALLET_SCREEN_CONTAINER" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index b724559114..f995dee52e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -16,6 +16,10 @@ object BigDecimalFormatter { private const val TEMP_CURRENCY_CODE = "USD" + private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") + private const val FIAT_MARKET_DEFAULT_DIGITS = 2 + private const val FIAT_MARKET_EXTENDED_DIGITS = 6 + fun formatCryptoAmount( cryptoAmount: BigDecimal?, cryptoCurrency: String, @@ -82,8 +86,43 @@ object BigDecimalFormatter { val formatterCurrency = getCurrency(fiatCurrencyCode) val formatter = NumberFormat.getCurrencyInstance(locale).apply { currency = formatterCurrency - maximumFractionDigits = 2 - minimumFractionDigits = 2 + maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + return if (fiatAmount.isLessThanThreshold()) { + buildString { + append(CAN_BE_LOWER_SIGN) + append( + formatter.format(FIAT_FORMAT_THRESHOLD) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), + ) + } + } else { + formatter.format(fiatAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } + } + + fun formatFiatAmountUncapped( + fiatAmount: BigDecimal?, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + if (fiatAmount == null) return EMPTY_BALANCE_SIGN + val formatterCurrency = getCurrency(fiatCurrencyCode) + + val digits = if (fiatAmount.isLessThanThreshold()) { + FIAT_MARKET_EXTENDED_DIGITS + } else { + FIAT_MARKET_DEFAULT_DIGITS + } + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = digits + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS roundingMode = RoundingMode.HALF_UP } @@ -141,4 +180,6 @@ object BigDecimalFormatter { } } } + + private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_joystream_22.xml b/core/ui/src/main/res/drawable/ic_joystream_22.xml new file mode 100644 index 0000000000..91f187f81c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_joystream_22.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_joystream_22.xml b/core/ui/src/main/res/drawable/img_joystream_22.xml new file mode 100644 index 0000000000..2168e5839e --- /dev/null +++ b/core/ui/src/main/res/drawable/img_joystream_22.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt index f15bce01ea..53cfb7ed29 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt @@ -8,6 +8,7 @@ import javax.inject.Inject interface CoroutineDispatcherProvider { val main: CoroutineDispatcher + val mainImmediate: CoroutineDispatcher val io: CoroutineDispatcher val default: CoroutineDispatcher val single: CoroutineDispatcher @@ -15,6 +16,7 @@ interface CoroutineDispatcherProvider { class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider { override val main: CoroutineDispatcher = Dispatchers.Main + override val mainImmediate: CoroutineDispatcher = Dispatchers.Main.immediate override val io: CoroutineDispatcher = Dispatchers.IO override val default: CoroutineDispatcher = Dispatchers.Default override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() @@ -22,6 +24,7 @@ class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcher class TestingCoroutineDispatcherProvider( override val main: CoroutineDispatcher = Dispatchers.Unconfined, + override val mainImmediate: CoroutineDispatcher = Dispatchers.Unconfined, override val io: CoroutineDispatcher = Dispatchers.Unconfined, override val default: CoroutineDispatcher = Dispatchers.Unconfined, override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(), diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt index ebbe1dc50a..cfa0536804 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt @@ -10,6 +10,13 @@ suspend fun runCatching(dispatcher: CoroutineDispatcher, block: suspend () - } } +suspend fun waitForDelay(delay: Long, block: suspend CoroutineScope.() -> R): R = coroutineScope { + val minWaitingTime = async { delay(delay) } + val actionInvoke = async { block() } + minWaitingTime.await() + actionInvoke.await() +} + class Debouncer { private var debounceJob: Job? = null diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt index 9930c5cb77..6ab240ea41 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt @@ -18,4 +18,9 @@ fun Collection.isSingleItem(): Boolean = this.size == 1 */ fun Collection.copy(): Collection { return this.map { it } +} + +inline fun List.indexOfFirstOrNull(predicate: (T) -> Boolean): Int? { + val index = indexOfFirst(predicate) + return if (index == -1) null else index } \ No newline at end of file diff --git a/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/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 9f6c1aa883..4bf2f7eb2a 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -3,6 +3,7 @@ package com.tangem.data.feedback import android.content.Context import android.content.pm.PackageInfo import android.os.Build +import com.tangem.blockchain.common.Blockchain import com.tangem.data.feedback.converters.BlockchainInfoConverter import com.tangem.data.feedback.converters.CardInfoConverter import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -13,6 +14,9 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import timber.log.Timber import java.io.File import java.io.FileWriter @@ -35,6 +39,8 @@ internal class DefaultFeedbackRepository( private val context: Context, ) : FeedbackRepository { + private val blockchainsErrors = MutableStateFlow>(emptyMap()) + override suspend fun getUserWalletsInfo(): UserWalletsInfo { return UserWalletsInfo( selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, @@ -52,6 +58,16 @@ internal class DefaultFeedbackRepository( .map(BlockchainInfoConverter::convert) } + override suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? { + return walletManagersStore + .getSyncOrNull( + userWalletId = getSelectedUserWallet().walletId, + blockchain = Blockchain.fromId(blockchainId), + derivationPath = derivationPath, + ) + ?.let(BlockchainInfoConverter::convert) + } + override fun getPhoneInfo(): PhoneInfo { return PhoneInfo( phoneModel = Build.MODEL, @@ -60,6 +76,22 @@ internal class DefaultFeedbackRepository( ) } + override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { + blockchainsErrors.update { + it.toMutableMap().apply { + put(getSelectedUserWallet().walletId, error) + } + } + } + + override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? { + return blockchainsErrors.value[getSelectedUserWallet().walletId].also { + if (it == null) { + Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}") + } + } + } + override suspend fun getAppLogs(): List { return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index ea61431f29..c4677126a6 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -6,7 +6,9 @@ import com.tangem.domain.qrscanning.models.QrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import com.tangem.domain.tokens.model.CryptoCurrency +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +import kotlinx.coroutines.yield import java.math.BigDecimal import java.net.URLDecoder @@ -14,15 +16,20 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { private data class QrScanningEvent(val type: SourceType, val qrCode: String) - private val scannedEvents = MutableSharedFlow() + private val scannedEvents = MutableSharedFlow(replay = 1) override suspend fun emitResult(type: SourceType, qrCode: String) { scannedEvents.emit(QrScanningEvent(type, qrCode)) } + @OptIn(ExperimentalCoroutinesApi::class) override fun subscribeToScanningResults(type: SourceType) = scannedEvents .filter { it.type == type } .map { it.qrCode } + .onEach { + yield() // if we have more than one sub, we must allow them to collect emitted value + scannedEvents.resetReplayCache() + } override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult { val withoutSchema = stripSchema(qrCode, cryptoCurrency) diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index f12116b4cf..96f4640242 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -15,8 +15,6 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.data.source.preferences) - implementation(projects.domain.balanceHiding.models) implementation(projects.domain.settings) diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 71e8f8d654..335d4b86fd 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -1,23 +1,25 @@ package com.tangem.data.settings -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import org.joda.time.DateTime internal class DefaultSettingsRepository( - private val preferencesDataSource: PreferencesDataSource, private val appPreferencesStore: AppPreferencesStore, - private val dispatchers: CoroutineDispatcherProvider, ) : SettingsRepository { override suspend fun shouldShowSaveUserWalletScreen(): Boolean { - return withContext(dispatchers.io) { preferencesDataSource.shouldShowSaveUserWalletScreen } + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, + default = true, + ) + } + + override suspend fun setShouldShowSaveUserWalletScreen(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_SAVE_USER_WALLET_SCREEN_KEY, value = value) } override suspend fun isWalletScrollPreviewEnabled(): Boolean { @@ -75,4 +77,38 @@ internal class DefaultSettingsRepository( value = isEnabled, ) } + + override suspend fun wasApplicationStopped(): Boolean { + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.WAS_APPLICATION_STOPPED_KEY, default = false) + } + + override suspend fun setWasApplicationStopped(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.WAS_APPLICATION_STOPPED_KEY, value = value) + } + + override suspend fun shouldOpenWelcomeScreenOnResume(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY, + default = false, + ) + } + + override suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_OPEN_WELCOME_ON_RESUME_KEY, value = value) + } + + override suspend fun shouldSaveAccessCodes(): Boolean { + return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, default = false) + } + + override suspend fun setShouldSaveAccessCodes(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, value = value) + } + + override suspend fun incrementAppLaunchCounter() { + appPreferencesStore.editData { preferences -> + val count = preferences.getOrDefault(key = PreferencesKeys.APP_LAUNCH_COUNT_KEY, default = 0) + preferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY] = count + 1 + } + } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 880f0d36b1..4f6bf8e17b 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -3,12 +3,10 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.data.settings.DefaultPromoSettingsRepository -import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,16 +19,8 @@ internal object SettingsDataModule { @Provides @Singleton - fun provideSettingsRepository( - preferencesDataSource: PreferencesDataSource, - appPreferencesStore: AppPreferencesStore, - dispatchers: CoroutineDispatcherProvider, - ): SettingsRepository { - return DefaultSettingsRepository( - preferencesDataSource = preferencesDataSource, - appPreferencesStore = appPreferencesStore, - dispatchers = dispatchers, - ) + fun provideSettingsRepository(appPreferencesStore: AppPreferencesStore): SettingsRepository { + return DefaultSettingsRepository(appPreferencesStore = appPreferencesStore) } @Provides diff --git a/data/source/preferences/build.gradle.kts b/data/source/preferences/build.gradle.kts deleted file mode 100644 index 3768a33a8c..0000000000 --- a/data/source/preferences/build.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -plugins { - alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.android.library) - id("configuration") -} - -android { - namespace = "com.tangem.data.source.preferences" -} - -dependencies { - implementation(deps.androidx.core.ktx) - implementation(deps.moshi) - implementation(deps.moshi.kotlin) - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - - // For MoshiJsonConverter - implementation(deps.tangem.card.core) -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt deleted file mode 100644 index e12d679fd7..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/PreferencesDataSource.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.data.source.preferences - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import javax.inject.Inject - -// 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories -// TODO: Replace shared preferences with DataStore -@Deprecated("Create repository instead") -class PreferencesDataSource @Inject internal constructor(applicationContext: Context) { - - private val preferences: SharedPreferences = - applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) - - init { - incrementLaunchCounter() - } - - var shouldShowSaveUserWalletScreen: Boolean - get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true) - set(value) = preferences.edit { - putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value) - } - - var shouldSaveAccessCodes: Boolean - get() = preferences.getBoolean(SAVE_ACCESS_CODES_KEY, false) - set(value) = preferences.edit { - putBoolean(SAVE_ACCESS_CODES_KEY, value) - } - - var wasApplicationStopped: Boolean - get() = preferences.getBoolean(APPLICATION_STOPPED_KEY, false) - set(value) = preferences.edit { - putBoolean(APPLICATION_STOPPED_KEY, value) - } - - var shouldOpenWelcomeScreenOnResume: Boolean - get() = preferences.getBoolean(OPEN_WELCOME_ON_RESUME_KEY, false) - set(value) = preferences.edit { - putBoolean(OPEN_WELCOME_ON_RESUME_KEY, value) - } - - private fun incrementLaunchCounter() { - var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0) - preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) } - } - - companion object { - private const val PREFERENCES_NAME = "tapPrefs" - private const val APP_LAUNCH_COUNT_KEY = "launchCount" - private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown" - private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes" - private const val APPLICATION_STOPPED_KEY = "applicationStopped" - private const val OPEN_WELCOME_ON_RESUME_KEY = "openWelcomeOnResume" - } -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/adapters/BigDecimalAdapter.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/adapters/BigDecimalAdapter.kt deleted file mode 100644 index d2adc38e1d..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/adapters/BigDecimalAdapter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.data.source.preferences.adapters - -import com.squareup.moshi.FromJson -import com.squareup.moshi.ToJson -import java.math.BigDecimal - -class BigDecimalAdapter { - @FromJson - fun fromJson(value: String) = BigDecimal(value) - - @ToJson - fun toJson(value: BigDecimal) = value.toString() -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/di/PreferencesStoreModule.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/di/PreferencesStoreModule.kt deleted file mode 100644 index 5961efde84..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/di/PreferencesStoreModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.data.source.preferences.di - -import android.content.Context -import com.tangem.data.source.preferences.PreferencesDataSource -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object PreferencesStoreModule { - - @Provides - @Singleton - fun providePreferencesStore(@ApplicationContext context: Context) = PreferencesDataSource(context) -} \ No newline at end of file diff --git a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/Migration.kt b/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/Migration.kt deleted file mode 100644 index e1cc25e4ba..0000000000 --- a/data/source/preferences/src/main/kotlin/com/tangem/data/source/preferences/storage/Migration.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.data.source.preferences.storage - -internal interface Migration { - fun migrate() -} \ No newline at end of file diff --git a/data/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 fe6ab4046f..28e83c756e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,7 +1,9 @@ package com.tangem.data.tokens.repository +import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory @@ -9,8 +11,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.CryptoCurrencyAddress @@ -22,10 +26,7 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.* import timber.log.Timber @Suppress("LongParameterList") @@ -43,6 +44,10 @@ internal class DefaultNetworksRepository( private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() } private val networkStatusFactory by lazy { NetworkStatusFactory() } + private val isNetworkStatusesFetching = MutableStateFlow( + value = emptyMap(), + ) + override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, @@ -58,6 +63,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) { @@ -105,15 +130,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..fd7493005d 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 @@ -92,6 +92,17 @@ class ResponseCryptoCurrenciesFactory { // [REDACTED_JIRA] Blockchain.Dischain, Blockchain.Arbitrum, + Blockchain.ArbitrumTestnet, + Blockchain.Aurora, + Blockchain.AuroraTestnet, + Blockchain.Manta, + Blockchain.MantaTestnet, + Blockchain.ZkSyncEra, + Blockchain.ZkSyncEraTestnet, + Blockchain.PolygonZkEVM, + Blockchain.PolygonZkEVMTestnet, + Blockchain.Base, + Blockchain.BaseTestnet, -> this.fullName else -> responseToken.name } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index 54f468860b..a944703601 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -2,8 +2,8 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency.ID import com.tangem.domain.tokens.model.Network diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt index db1650880f..ec40d6e52c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensBackwardCompatibility.kt @@ -1,9 +1,9 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.toCoinId /** * Helper to apply compatibility changes for [UserTokensResponse] to support old saved tokens diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index 7e75db2896..7e8372da6c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -1,7 +1,7 @@ package com.tangem.data.tokens.utils +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency class UserTokensResponseFactory { diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index e1b448fb10..615758dcf6 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -16,15 +16,19 @@ dependencies { implementation(deps.tangem.blockchain) /** Core */ + implementation(projects.core.datasource) implementation(projects.core.utils) /** Domain */ implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + implementation(deps.timber) } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 3fe078eaad..96b20e9c0c 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -4,22 +4,26 @@ import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.hedera.HederaTransactionBuilder +import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber +import java.math.BigDecimal internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, + private val walletManagersStore: WalletManagersStore, private val coroutineDispatcherProvider: CoroutineDispatcherProvider, ) : TransactionRepository { @@ -30,6 +34,8 @@ internal class DefaultTransactionRepository( destination: String, userWalletId: UserWalletId, network: Network, + isSwap: Boolean, + hash: String?, ): TransactionData? = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( @@ -38,11 +44,54 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) - return@withContext walletManager?.createTransaction(amount, fee, destination)?.copy( - extras = getMemoExtras(network.id.value, memo), + return@withContext walletManager?.createTransactionInternal( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + network = network, + isSwap = isSwap, + hash = hash, ) } + override suspend fun validateTransaction( + amount: Amount, + fee: Fee?, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean, + hash: String?, + ): Result { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersStore.getSyncOrNull( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + val validator = walletManager as? TransactionValidator + + return if (validator != null) { + val transaction = walletManager.createTransactionInternal( + amount = amount, + fee = fee ?: Fee.Common(amount = amount), + memo = memo, + destination = destination, + network = network, + isSwap = isSwap, + hash = hash, + ) + + validator.validate(transaction = transaction) + } else { + Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation") + Result.success(Unit) + } + } + override suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, @@ -58,6 +107,28 @@ internal class DefaultTransactionRepository( (walletManager as TransactionSender).send(txData, signer) } + @Suppress("LongParameterList") + private fun WalletManager.createTransactionInternal( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + network: Network, + isSwap: Boolean, + hash: String?, + ): TransactionData { + val txAmount = if (isSwap) { + createAmountForSwap(amount) + } else { + amount + } + + return createTransaction(txAmount, fee, destination).copy( + hash = hash, + extras = getMemoExtras(network.id.value, memo), + ) + } + private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) if (memo == null) return null @@ -76,9 +147,24 @@ internal class DefaultTransactionRepository( Blockchain.TerraV2, -> CosmosTransactionExtras(memo) Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionBuilder.HederaTransactionExtras(memo) + Blockchain.Hedera -> HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) else -> null } } + + private fun createAmountForSwap(amount: Amount): Amount { + return when (amount.type) { + is AmountType.Coin -> amount + else -> { + // 1. when creates swap amount for NonNativeToken, amount should be ZERO + // 2. Amount has .Coin type, as workaround to use destinationAddress in bsdk, not contractAddress + Amount( + currencySymbol = amount.currencySymbol, + value = BigDecimal.ZERO, + decimals = amount.decimals, + ) + } + } + } } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index c8949d39c0..a6d7043d91 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.transaction.di import com.tangem.data.transaction.DefaultFeeRepository import com.tangem.data.transaction.DefaultTransactionRepository +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -20,10 +21,12 @@ internal object TransactionDataModule { @Singleton fun providesTransactionRepository( walletManagersFacade: WalletManagersFacade, + walletManagersStore: WalletManagersStore, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): TransactionRepository { return DefaultTransactionRepository( walletManagersFacade = walletManagersFacade, + walletManagersStore = walletManagersStore, coroutineDispatcherProvider = coroutineDispatcherProvider, ) } diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 2b5b2f8165..3a8e769a69 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.datasource) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index eada356d1e..46d2cbef79 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -85,7 +85,7 @@ class DefaultTxHistoryRepository( network = network, ) val lastTxHash = walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty() - return when (val txExploreState = blockchain?.getExploreTxUrl(lastTxHash)) { + return when (val txExploreState = blockchain.getExploreTxUrl(lastTxHash)) { is TxExploreState.Url -> txExploreState.url else -> "" } diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index ac55319dfe..e552eb58b0 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { /** Project - Utils */ implementation(projects.core.utils) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) /** Project - Libs */ debugImplementation(projects.libs.visa) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletNamesMigrationRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletNamesMigrationRepository.kt new file mode 100644 index 0000000000..60d442028e --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletNamesMigrationRepository.kt @@ -0,0 +1,23 @@ +package com.tangem.data.wallets + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository + +class DefaultWalletNamesMigrationRepository( + private val appPreferencesStore: AppPreferencesStore, +) : WalletNamesMigrationRepository { + + override suspend fun isMigrationDone(): Boolean { + return appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.IS_WALLET_NAMES_MIGRATION_DONE_KEY, + default = false, + ) + } + + override suspend fun setMigrationDone() { + appPreferencesStore.store(PreferencesKeys.IS_WALLET_NAMES_MIGRATION_DONE_KEY, true) + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 26e9f82859..9748691618 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -1,9 +1,11 @@ package com.tangem.data.wallets.di +import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletAddressServiceRepository import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletAddressServiceRepository import com.tangem.domain.wallets.repository.WalletsRepository import dagger.Module @@ -29,4 +31,10 @@ internal object WalletsDataModule { ): WalletAddressServiceRepository { return DefaultWalletAddressServiceRepository(walletManagersFacade) } + + @Provides + @Singleton + fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository { + return DefaultWalletNamesMigrationRepository(appPreferencesStore) + } } \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 4ec562467f..d317bafbf5 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.core) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) // TODO: Remove after new card scan result was implemented implementation(projects.domain.models) implementation(projects.domain.tokens.models) diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt deleted file mode 100644 index 538ac0f240..0000000000 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.domain.card - -import com.tangem.domain.card.repository.CardSdkConfigRepository - -/** - * Use case for getting access code saving status - * - * @property cardSdkConfigRepository repository for managing of CardSDK config - * -[REDACTED_AUTHOR] - */ -class GetAccessCodeSavingStatusUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { - - operator fun invoke(): Boolean = cardSdkConfigRepository.isAccessCodeSavingEnabled() -} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/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/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 3e7859b754..6a21560035 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -13,4 +13,5 @@ dependencies { implementation(deps.jodatime) implementation(projects.core.res) + implementation(projects.domain.wallets.models) } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index 5350902e33..a671bad033 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -1,9 +1,6 @@ package com.tangem.domain.feedback -import com.tangem.domain.feedback.models.BlockchainInfo -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.feedback.models.PhoneInfo -import com.tangem.domain.feedback.models.UserWalletsInfo +import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.utils.breakLine import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses @@ -68,6 +65,24 @@ internal class FeedbackDataBuilder { builder.appendKeyValue("App version", phoneInfo.appVersion) } + fun addBlockchainError(info: BlockchainInfo, error: BlockchainErrorInfo) { + builder.appendKeyValue("Blockchain", info.blockchain) + builder.appendKeyValue("Derivation path", info.derivationPath) + builder.appendKeyValue("Host", info.host) + builder.appendKeyValue("Token", error.tokenSymbol) + builder.appendKeyValue("Error", error.errorMessage) + + builder.appendDelimiter() + + builder.appendAddresses( + key = "Source address${info.addresses.isMultiple(suffix = "es")}", + addresses = info.addresses, + ) + builder.appendKeyValue("Destination address", error.destinationAddress) + builder.appendKeyValue("Amount", error.amount) + builder.appendKeyValue("Fee", error.fee ?: "Unable to receive") + } + fun addDelimiter(): StringBuilder = builder.appendDelimiter() fun build(): String = builder.trimEnd().toString() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt new file mode 100644 index 0000000000..50998a4ed8 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.feedback + +import android.content.res.Resources +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.FeedbackEmail +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.feedback.utils.* + +/** + * Get email with feedback for support + * + * @property feedbackRepository feedback repository + * @property resources resources for getting strings + * +[REDACTED_AUTHOR] + */ +class GetFeedbackEmailUseCase( + private val feedbackRepository: FeedbackRepository, + private val resources: Resources, +) { + + private val emailSubjectResolver = EmailSubjectResolver(resources) + private val emailMessageTitleResolver = EmailMessageTitleResolver(resources) + private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository) + + suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail { + val cardInfo = feedbackRepository.getCardInfo() + + val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs()) + + return FeedbackEmail( + address = getAddress(cardInfo), + subject = emailSubjectResolver.resolve(type, cardInfo), + message = createMessage(type, cardInfo), + file = feedbackRepository.createLogFile(logs = formattedLogs), + ) + } + + private fun getAddress(cardInfo: CardInfo): String { + return if (cardInfo.isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL + } + + private suspend fun createMessage(type: FeedbackEmailType, cardInfo: CardInfo): String { + return StringBuilder().apply { + val title = emailMessageTitleResolver.resolve(type) + append(title) + + skipLine() + + appendDisclaimerIfNeeded(type) + + skipLine() + + val body = emailMessageBodyResolver.resolve(type, cardInfo) + append(body) + }.toString() + } + + private fun StringBuilder.appendDisclaimerIfNeeded(type: FeedbackEmailType): StringBuilder { + return if (type is FeedbackEmailType.ScanningProblem) { + this + } else { + append(resources.getString(R.string.feedback_data_collection_message)) + } + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt deleted file mode 100644 index 6ab491c734..0000000000 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.domain.feedback - -import android.content.res.Resources -import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.feedback.models.SupportFeedbackEmail -import com.tangem.domain.feedback.repository.FeedbackRepository -import com.tangem.domain.feedback.utils.skipLine -import org.joda.time.DateTime -import org.joda.time.format.DateTimeFormatterBuilder -import java.util.Locale - -/** - * Get email with feedback for support - * - * @property feedbackRepository feedback repository - * @property resources resources for getting strings - * -[REDACTED_AUTHOR] - */ -class GetSupportFeedbackEmailUseCase( - private val feedbackRepository: FeedbackRepository, - private val resources: Resources, -) { - - // 00.00 00:00:00.000 - private val dateFormatter = DateTimeFormatterBuilder() - .appendDayOfMonth(2) - .appendLiteral('.') - .appendMonthOfYear(2) - .appendLiteral(' ') - .appendHourOfDay(2) - .appendLiteral(':') - .appendMinuteOfHour(2) - .appendLiteral(':') - .appendSecondOfMinute(2) - .appendLiteral('.') - .appendMillisOfSecond(3) - .toFormatter() - .withLocale(Locale.getDefault()) - - suspend operator fun invoke(): SupportFeedbackEmail { - val cardInfo = feedbackRepository.getCardInfo() - - return SupportFeedbackEmail( - address = getEmail(isStart2Coin = cardInfo.isStart2Coin), - subject = getSubject(isStart2Coin = cardInfo.isStart2Coin), - message = createMessage(cardInfo), - file = feedbackRepository.createLogFile(logs = getLogs()), - ) - } - - private fun getEmail(isStart2Coin: Boolean): String { - return if (isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL - } - - private fun getSubject(isStart2Coin: Boolean): String { - return resources.getString( - if (isStart2Coin) { - R.string.feedback_subject_support - } else { - R.string.feedback_subject_support_tangem - }, - ) - } - - private suspend fun createMessage(cardInfo: CardInfo): String { - return StringBuilder().apply { - append(resources.getString(R.string.feedback_preface_support)) - skipLine() - append(resources.getString(R.string.feedback_data_collection_message)) - skipLine() - append( - FeedbackDataBuilder().apply { - addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) - addDelimiter() - addCardInfo(cardInfo) - addDelimiter() - addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) - addDelimiter() - addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) - }.build(), - ) - }.toString() - } - - private suspend fun getLogs(): String { - val builder = StringBuilder() - - var sum = 0 - val appLogs = feedbackRepository.getAppLogs() - for (i in appLogs.lastIndex downTo 0) { - val log = appLogs[i] - val date = dateFormatter.print(DateTime(log.timestamp)) - - val formattedLog = "$date: ${log.message}\n" - - sum += formattedLog.length - if (sum < GMAIL_MAX_FILE_SIZE) { - builder.insert(0, formattedLog) - } else { - break - } - } - - return builder.toString() - } - - private companion object { - const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" - const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" - const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB - } -} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt new file mode 100644 index 0000000000..596699a073 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SaveBlockchainErrorUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.repository.FeedbackRepository + +/** + * Save last blockchain error + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +class SaveBlockchainErrorUseCase( + private val feedbackRepository: FeedbackRepository, +) { + + fun invoke(error: BlockchainErrorInfo) { + feedbackRepository.saveBlockchainErrorInfo(error = error) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt new file mode 100644 index 0000000000..fc76ba451b --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.feedback.models + +/** + * Information about blockchain's operation error + * + * @property errorMessage message about error + * @property blockchainId blockchain id + * @property derivationPath derivation path + * @property destinationAddress destination address + * @property tokenSymbol token symbol or null, if it isn't operation with token + * @property amount amount + * @property fee fee or null, if unable to get + */ +data class BlockchainErrorInfo( + val errorMessage: String, + val blockchainId: String, + val derivationPath: String?, + val destinationAddress: String, + val tokenSymbol: String?, + val amount: String, + val fee: String?, +) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmail.kt similarity index 83% rename from domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt rename to domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmail.kt index 35faf18f6f..9187f590f0 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmail.kt @@ -2,7 +2,7 @@ package com.tangem.domain.feedback.models import java.io.File -data class SupportFeedbackEmail( +data class FeedbackEmail( val address: String, val subject: String, val message: String, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt new file mode 100644 index 0000000000..6bbd5a9670 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.feedback.models + +/** + * Email feedback type + * +[REDACTED_AUTHOR] + */ +sealed interface FeedbackEmailType { + + /** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */ + data object DirectUserRequest : FeedbackEmailType + + /** User rate the app as "can be better" */ + data object RateCanBeBetter : FeedbackEmailType + + /** User has problem with scanning */ + data object ScanningProblem : FeedbackEmailType + + /** User has problem with sending transaction */ + data object TransactionSendingProblem : FeedbackEmailType +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index 58281067c1..c9d6806eea 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -11,8 +11,14 @@ interface FeedbackRepository { suspend fun getBlockchainInfoList(): List + suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? + fun getPhoneInfo(): PhoneInfo + fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) + + suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? + suspend fun getAppLogs(): List suspend fun createLogFile(logs: String): File? diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt new file mode 100644 index 0000000000..e5596b2a78 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/AppLogsFormatter.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.feedback.utils + +import com.tangem.domain.feedback.models.AppLogModel +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormatter +import org.joda.time.format.DateTimeFormatterBuilder +import java.util.Locale + +/** + * App logs formatter + * +[REDACTED_AUTHOR] + */ +internal class AppLogsFormatter { + + private val dateFormatter = createDateFormatter() + + /** Format [appLogs] to [String] */ + fun format(appLogs: List): String { + val builder = StringBuilder() + + var sum = 0 + for (i in appLogs.lastIndex downTo 0) { + val log = appLogs[i] + val date = dateFormatter.print(DateTime(log.timestamp)) + + val formattedLog = "$date: ${log.message}\n" + + sum += formattedLog.length + if (sum < GMAIL_MAX_FILE_SIZE) { + builder.insert(0, formattedLog) + } else { + break + } + } + + return builder.toString() + } + + // Example, 00.00 00:00:00.000 + private fun createDateFormatter(): DateTimeFormatter { + return DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral('.') + .appendMonthOfYear(2) + .appendLiteral(' ') + .appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2) + .appendLiteral('.') + .appendMillisOfSecond(MIN_MILLIS_DIGITS) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + private companion object { + const val MIN_MILLIS_DIGITS = 3 + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt new file mode 100644 index 0000000000..8574fb1010 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -0,0 +1,70 @@ +package com.tangem.domain.feedback.utils + +import com.tangem.domain.feedback.FeedbackDataBuilder +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.feedback.repository.FeedbackRepository + +/** + * Email message body resolver + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +internal class EmailMessageBodyResolver( + private val feedbackRepository: FeedbackRepository, +) { + + /** Resolve email message body by [type] using [cardInfo] */ + suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) { + when (type) { + FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo) + FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo) + FeedbackEmailType.ScanningProblem -> addScanningProblemBody() + FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo) + } + + return build() + } + + private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) + addDelimiter() + addCardInfo(cardInfo) + addDelimiter() + addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private fun FeedbackDataBuilder.addScanningProblemBody() { + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(cardInfo: CardInfo) { + addCardInfo(cardInfo) + addDelimiter() + + val blockchainError = feedbackRepository.getBlockchainErrorInfo() + val blockchainInfo = blockchainError?.let { + feedbackRepository.getBlockchainInfo( + blockchainId = blockchainError.blockchainId, + derivationPath = blockchainError.derivationPath, + ) + } + + if (blockchainInfo != null) { + addBlockchainError(blockchainInfo, blockchainError) + addDelimiter() + } + + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } + + private fun FeedbackDataBuilder.addCardAndPhoneInfo(cardInfo: CardInfo) { + addCardInfo(cardInfo) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt new file mode 100644 index 0000000000..38c983c617 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.feedback.utils + +import android.content.res.Resources +import com.tangem.domain.feedback.R +import com.tangem.domain.feedback.models.FeedbackEmailType + +/** + * Email message title resolver + * + * @property resources resources + * +[REDACTED_AUTHOR] + */ +internal class EmailMessageTitleResolver(private val resources: Resources) { + + /** Resolve email message title by [type] */ + fun resolve(type: FeedbackEmailType): String { + return when (type) { + FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support + FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative + FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed + FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + } + .let(resources::getString) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt new file mode 100644 index 0000000000..392fa7d2f4 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.feedback.utils + +import android.content.res.Resources +import com.tangem.domain.feedback.R +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.FeedbackEmailType + +/** + * Email subject resolver + * + * @property resources resources + * +[REDACTED_AUTHOR] + */ +internal class EmailSubjectResolver(private val resources: Resources) { + + /** Resolve email message body by [type] using [cardInfo] */ + fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String { + return when (type) { + FeedbackEmailType.DirectUserRequest -> { + if (cardInfo.isStart2Coin) { + R.string.feedback_subject_support + } else { + R.string.feedback_subject_support_tangem + } + } + FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative + FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed + FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + } + .let(resources::getString) + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt new file mode 100644 index 0000000000..aeaa917e5f --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/FeedbackConstants.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.feedback.utils + +internal const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB + +internal const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" +internal const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index bc0e716a7a..f01a2e6a96 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -9,16 +9,17 @@ android { } dependencies { - implementation(project(":core:datasource")) - implementation(project(":core:utils")) - implementation(project(":common")) - implementation(project(":libs:auth")) + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.common) + implementation(projects.libs.auth) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.demo) implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) - /** Tangem libraries */ implementation(deps.tangem.blockchain) { exclude(module = "joda-time") diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 1f0b7b7b51..9373ae2a5b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -20,6 +20,5 @@ object NetworkLogConfig { object AnalyticsHandlersLogConfig { const val firebase: Boolean = false - const val appsFlyer: Boolean = false val amplitude: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index aea826fa7c..cf827af989 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -1,6 +1,7 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.isSupportedInApp import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.CardTypesResolver @@ -22,7 +23,7 @@ fun CardDTO.supportedBlockchains(cardTypesResolver: CardTypesResolver): List TransactionHistoryRequest.FilterType.Coin - is CryptoCurrency.Token -> TransactionHistoryRequest.FilterType.Contract(currency.contractAddress) + is CryptoCurrency.Token -> { + val blockchainToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + id = currency.id.rawCurrencyId, + ) + TransactionHistoryRequest.FilterType.Contract(blockchainToken) + } }, ) .let(txHistoryStateConverter::convert) @@ -221,7 +218,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) + } }, ), ) @@ -573,6 +579,34 @@ class DefaultWalletManagersFacade( ) } + override suspend fun getAssetRequirements( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): AssetRequirementsCondition? { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) + if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null + + val condition = walletManager.requirementsCondition(currencyType) ?: return null + return requirementsConditionConverter.convert(condition) + } + + override suspend fun associateAsset( + userWalletId: UserWalletId, + currency: CryptoCurrency, + signer: CommonSigner, + ): SimpleResult { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) + val currencyType = cryptoCurrencyTypeConverter.convert(currency) + + if (walletManager !is AssetRequirementsManager) { + return SimpleResult.Failure( + BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"), + ) + } + return walletManager.fulfillRequirements(currencyType, signer) + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 4636317765..5c80c0db3d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -13,6 +13,7 @@ import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState @@ -240,4 +241,16 @@ interface WalletManagersFacade { decimals: Int, id: String? = null, ): BigDecimal + + /** + * Get requirements for asset(currency) + * @return null if there's no requirement, otherwise [AssetRequirementsCondition]. + */ + suspend fun getAssetRequirements(userWalletId: UserWalletId, currency: CryptoCurrency): AssetRequirementsCondition? + + suspend fun associateAsset( + userWalletId: UserWalletId, + currency: CryptoCurrency, + signer: CommonSigner, + ): SimpleResult } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt new file mode 100644 index 0000000000..53d7f303c5 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/CryptoCurrencyTypeConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.CryptoCurrencyType +import com.tangem.blockchain.common.Token +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.converter.Converter + +internal class CryptoCurrencyTypeConverter : Converter { + override fun convert(value: CryptoCurrency): CryptoCurrencyType { + return when (value) { + is CryptoCurrency.Coin -> CryptoCurrencyType.Coin + is CryptoCurrency.Token -> CryptoCurrencyType.Token( + info = Token( + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + id = value.id.rawCurrencyId, + ), + ) + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt new file mode 100644 index 0000000000..7c84120470 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkRequirementsConditionConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.domain.transaction.models.AssetRequirementsCondition +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.trustlines.AssetRequirementsCondition as SdkRequirementsCondition + +internal class SdkRequirementsConditionConverter : Converter { + override fun convert(value: SdkRequirementsCondition): AssetRequirementsCondition { + return when (value) { + SdkRequirementsCondition.PaidTransaction -> AssetRequirementsCondition.PaidTransaction + is SdkRequirementsCondition.PaidTransactionWithFee -> AssetRequirementsCondition.PaidTransactionWithFee( + feeAmount = requireNotNull(value.feeAmount.value), + feeCurrencySymbol = value.feeAmount.currencySymbol, + decimals = value.feeAmount.decimals, + ) + } + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionHistoryItemConverter.kt index 2a8c4248b7..2e1fba8fe9 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 @@ -58,7 +58,9 @@ internal class SdkTransactionHistoryItemConverter( } else { mapToInteractionAddressType(sourceType = sourceType) } - is SdkTransactionHistoryItem.TransactionType.ContractMethod -> mapToInteractionAddressType(destinationType) + is SdkTransactionHistoryItem.TransactionType.ContractMethod, + is SdkTransactionHistoryItem.TransactionType.ContractMethodName, + -> mapToInteractionAddressType(destinationType) } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt index 67019252a1..ac4402be26 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,12 +4,12 @@ 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 -class SdkTransactionTypeConverter( +internal class SdkTransactionTypeConverter( private val assetReader: AssetReader, private val moshi: Moshi, ) : Converter { @@ -27,16 +27,20 @@ class SdkTransactionTypeConverter( override fun convert(value: TransactionHistoryItem.TransactionType): TxHistoryItem.TransactionType { return when (value) { - TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer - is TransactionHistoryItem.TransactionType.ContractMethod -> { - return when (val name = smartContractMethods[value.id]?.name) { - "transfer" -> TxHistoryItem.TransactionType.Transfer - "approve" -> TxHistoryItem.TransactionType.Approve - "swap" -> TxHistoryItem.TransactionType.Swap - null -> TxHistoryItem.TransactionType.UnknownOperation - else -> TxHistoryItem.TransactionType.Operation(name = name.replaceFirstChar { it.titlecase() }) - } - } + is TransactionHistoryItem.TransactionType.ContractMethod -> + getTransactionType(methodName = smartContractMethods[value.id]?.name) + is TransactionHistoryItem.TransactionType.ContractMethodName -> getTransactionType(methodName = value.name) + is TransactionHistoryItem.TransactionType.Transfer -> TxHistoryItem.TransactionType.Transfer + } + } + + private fun getTransactionType(methodName: String?): TxHistoryItem.TransactionType { + return when (methodName) { + "transfer" -> TxHistoryItem.TransactionType.Transfer + "approve" -> TxHistoryItem.TransactionType.Approve + "swap" -> TxHistoryItem.TransactionType.Swap + null -> TxHistoryItem.TransactionType.UnknownOperation + else -> TxHistoryItem.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() }) } } 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 3e0bb9743c..de3ef835c5 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -11,11 +11,13 @@ android { dependencies { /** Project - Domain */ - implementation(projects.domain.core) + api(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) implementation(projects.domain.settings) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 58eaf3cff6..6b098d5be3 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -15,7 +15,7 @@ import java.math.BigDecimal */ data class CryptoCurrencyStatus( val currency: CryptoCurrency, - val value: Status, + val value: Value, ) { /** @@ -23,7 +23,7 @@ data class CryptoCurrencyStatus( * * @property isError Indicates whether this status represents an error status. */ - sealed class Status(val isError: Boolean) { + sealed class Value(val isError: Boolean) { /** The amount of the cryptocurrency. */ open val amount: BigDecimal? = null @@ -48,7 +48,7 @@ data class CryptoCurrencyStatus( } /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ - object Loading : Status(isError = false) + data object Loading : Value(isError = false) /** * Represents a state where the cryptocurrency is not reachable. @@ -61,19 +61,19 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, override val networkAddress: NetworkAddress?, - ) : Status(isError = true) + ) : Value(isError = true) /** Represents a state where the cryptocurrency's network amount not found. */ data class NoAmount( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, - ) : Status(isError = true) + ) : Value(isError = true) /** Represents a state where the cryptocurrency's derivation is missed. */ data class MissedDerivation( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, - ) : Status(isError = true) + ) : Value(isError = true) /** * Represents a state where there is no account associated with the cryptocurrency @@ -86,7 +86,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, override val networkAddress: NetworkAddress, - ) : Status(isError = false) { + ) : Value(isError = false) { override val amount: BigDecimal = BigDecimal.ZERO } @@ -110,7 +110,7 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, - ) : Status(isError = false) + ) : Value(isError = false) /** * Represents a Custom state of a cryptocurrency, typically used for user-defined tokens. @@ -131,7 +131,7 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, - ) : Status(isError = false) + ) : Value(isError = false) /** * Represents a state where the cryptocurrency is available, but there is no current quote available for it. @@ -146,5 +146,5 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, - ) : Status(isError = false) + ) : Value(isError = false) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt index 60fdf9e7b8..08a746b12e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -11,7 +11,7 @@ import java.math.BigDecimal */ data class NetworkStatus( val network: Network, - val value: Status, + val value: Value, ) { /** @@ -19,19 +19,19 @@ data class NetworkStatus( * * This sealed class includes different states like unreachable, missed derivation, verified, and no account. */ - sealed class Status + sealed class Value /** * Represents the state where the network is unreachable. * * @property address Network addresses. */ - data class Unreachable(val address: NetworkAddress?) : Status() + data class Unreachable(val address: NetworkAddress?) : Value() /** * Represents the state where a derivation has been missed. */ - object MissedDerivation : Status() + data object MissedDerivation : Value() /** * Represents the verified state of the network, including the amounts associated with different cryptocurrencies @@ -46,7 +46,7 @@ data class NetworkStatus( val address: NetworkAddress, val amounts: Map, val pendingTransactions: Map>, - ) : Status() + ) : Value() /** * Represents the state where there is no account, and an amount is required to create one. @@ -59,5 +59,5 @@ data class NetworkStatus( val address: NetworkAddress, val amountToCreateAccount: BigDecimal, val errorMessage: String, - ) : Status() + ) : Value() } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index 36d186b41d..86b0059faf 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -31,7 +31,7 @@ sealed class CryptoCurrencyWarning { val amountCurrency: CryptoCurrency, ) : CryptoCurrencyWarning() - object TopUpWithoutReserve : CryptoCurrencyWarning() + data object TopUpWithoutReserve : CryptoCurrencyWarning() /** * Represents wallet blockchain rent @@ -41,8 +41,6 @@ sealed class CryptoCurrencyWarning { */ data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning() - data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning() - data class SwapPromo( val startDateTime: DateTime, val endDateTime: DateTime, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt new file mode 100644 index 0000000000..e251aae9a0 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/HederaWarnings.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.tokens.model.warnings + +import com.tangem.domain.tokens.model.CryptoCurrency +import java.math.BigDecimal + +sealed class HederaWarnings : CryptoCurrencyWarning() { + + abstract val currency: CryptoCurrency + + data class AssociateWarning(override val currency: CryptoCurrency) : HederaWarnings() + + data class AssociateWarningWithFee( + override val currency: CryptoCurrency, + val fee: BigDecimal, + val feeCurrencySymbol: String, + val feeCurrencyDecimals: Int, + ) : HederaWarnings() +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt index ed01f84f51..171f60eb9e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenScreenAnalyticsEvent.kt @@ -72,4 +72,9 @@ sealed class TokenScreenAnalyticsEvent( event = "Token Bought", params = mapOf("Token" to token), ) + + class Associate(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent( + event = "Button - Token Trustline", + params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt index efe0126403..31b6ccc631 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.tokens -import arrow.core.Either import arrow.core.left +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -12,19 +12,19 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.transformLatest class GetCardTokensListUseCase( - internal val currenciesRepository: CurrenciesRepository, - internal val quotesRepository: QuotesRepository, - internal val networksRepository: NetworksRepository, - internal val dispatchers: CoroutineDispatcherProvider, + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): Flow> { + operator fun invoke(userWalletId: UserWalletId): EitherFlow { return getTokensStatuses(userWalletId).transformLatest { maybeTokens -> maybeTokens.fold( ifLeft = { error -> @@ -37,9 +37,7 @@ class GetCardTokensListUseCase( } } - private fun getTokensStatuses( - userWalletId: UserWalletId, - ): Flow>> { + private fun getTokensStatuses(userWalletId: UserWalletId): EitherFlow> { val operations = CurrenciesStatusesOperations( userWalletId = userWalletId, currenciesRepository = currenciesRepository, @@ -56,7 +54,7 @@ class GetCardTokensListUseCase( private fun createTokenList( userWalletId: UserWalletId, tokens: List, - ): Flow> { + ): EitherFlow { val operations = TokenListOperations( userWalletId = userWalletId, tokens = tokens, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 07cdfdabdc..e521d035af 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -8,14 +8,12 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import java.math.BigDecimal /** * Use case to determine which TokenActions are available for a [CryptoCurrency] @@ -25,16 +23,19 @@ import java.math.BigDecimal @Suppress("LongParameterList") class GetCryptoCurrencyActionsUseCase( private val rampManager: RampStateManager, + private val walletManagersFacade: WalletManagersFacade, private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, - private val sendFeatureToggles: SendFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { + suspend operator fun invoke( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Flow { val operations = CurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -42,7 +43,7 @@ class GetCryptoCurrencyActionsUseCase( userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id - + val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency) return flow { val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId) @@ -57,6 +58,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, coinStatus = maybeCoinStatus.getOrNull(), cryptoCurrencyStatus = cryptoCurrencyStatus, + needAssociateAsset = requirements != null, ) } @@ -68,33 +70,37 @@ class GetCryptoCurrencyActionsUseCase( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): TokenActionsState { return TokenActionsState( walletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus, states = createListOfActions( - userWallet, - coinStatus, - cryptoCurrencyStatus, + userWallet = userWallet, + coinStatus = coinStatus, + cryptoCurrencyStatus = cryptoCurrencyStatus, + needAssociateAsset = needAssociateAsset, ), ) } /** * Creates list of action for expected order - * Actions priority: [Buy Send Receive Sell Swap] + * Actions priority: [Receive Send Swap Buy Sell] */ + @Suppress("CyclomaticComplexMethod", "LongMethod") private suspend fun createListOfActions( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): List { val cryptoCurrency = cryptoCurrencyStatus.currency if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { - return listOf(TokenActionsState.ActionState.HideToken(true)) + return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) } if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) { - return getActionsForUnreachableCurrency(cryptoCurrencyStatus) + return getActionsForUnreachableCurrency(cryptoCurrencyStatus, needAssociateAsset) } val activeList = mutableListOf() @@ -102,115 +108,155 @@ class GetCryptoCurrencyActionsUseCase( // copy address if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.CopyAddress(true)) + activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) } // receive if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.Receive(true)) + val scenario = if (needAssociateAsset) { + ScenarioUnavailabilityReason.UnassociatedAsset + } else { + ScenarioUnavailabilityReason.None + } + activeList.add(TokenActionsState.ActionState.Receive(scenario)) } // send - if ( - isSendDisabled( - userWalletId = userWallet.walletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) - ) { - disabledList.add(TokenActionsState.ActionState.Send(false)) + val sendUnavailabilityReason = getSendUnavailabilityReason( + cryptoCurrencyStatus = cryptoCurrencyStatus, + coinStatus = coinStatus, + ) + if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason)) } else { - activeList.add(TokenActionsState.ActionState.Send(true)) + disabledList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason)) } // swap if (userWallet.isMultiCurrency) { - if (marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Swap(true)) + if ( + marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency) && + cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote + ) { + activeList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None)) } else { - disabledList.add(TokenActionsState.ActionState.Swap(false)) + disabledList.add( + TokenActionsState.ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.NotExchangeable(cryptoCurrency.name), + ), + ) } } // buy if (rampManager.availableForBuy(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Buy(true)) + activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { - disabledList.add(TokenActionsState.ActionState.Buy(false)) + disabledList.add( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable(cryptoCurrency.symbol)), + ) } // sell - if (rampManager.availableForSell(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Sell(true)) - } else { - disabledList.add(TokenActionsState.ActionState.Sell(false)) + val sellSupportedByService = rampManager.availableForSell(cryptoCurrency) + val sendAvailable = sendUnavailabilityReason is ScenarioUnavailabilityReason.None + + when { + sellSupportedByService && sendAvailable -> { + activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)) + } + sellSupportedByService && !sendAvailable -> { + (sendUnavailabilityReason as? ScenarioUnavailabilityReason.EmptyBalance)?.let { + disabledList.add( + TokenActionsState.ActionState.Sell( + unavailabilityReason = it.copy( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL, + ), + ), + ) + } + (sendUnavailabilityReason as? ScenarioUnavailabilityReason.PendingTransaction)?.let { + disabledList.add( + TokenActionsState.ActionState.Sell( + unavailabilityReason = it.copy( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SELL, + ), + ), + ) + } + } + else -> { + disabledList.add( + TokenActionsState.ActionState.Sell( + unavailabilityReason = ScenarioUnavailabilityReason.NotSupportedBySellService( + cryptoCurrency.name, + ), + ), + ) + } } // hide - activeList.add(TokenActionsState.ActionState.HideToken(true)) + activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return activeList + disabledList } private fun getActionsForUnreachableCurrency( cryptoCurrencyStatus: CryptoCurrencyStatus, + needAssociateAsset: Boolean, ): List { - val activeList = mutableListOf() - val disabledList = mutableListOf() + val actionsList = mutableListOf() if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.CopyAddress(true)) + actionsList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) } if (rampManager.availableForBuy(cryptoCurrencyStatus.currency)) { - activeList.add(TokenActionsState.ActionState.Buy(true)) + actionsList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { - disabledList.add(TokenActionsState.ActionState.Buy(false)) + actionsList.add( + TokenActionsState.ActionState.Buy( + ScenarioUnavailabilityReason.BuyUnavailable( + cryptoCurrencyName = cryptoCurrencyStatus.currency.name, + ), + ), + ) } - disabledList.add(TokenActionsState.ActionState.Send(false)) - disabledList.add(TokenActionsState.ActionState.Swap(false)) - disabledList.add(TokenActionsState.ActionState.Sell(false)) + actionsList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable)) + actionsList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.Unreachable)) + actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.Receive(true)) + val scenario = if (needAssociateAsset) { + ScenarioUnavailabilityReason.UnassociatedAsset + } else { + ScenarioUnavailabilityReason.None + } + actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } - activeList.add(TokenActionsState.ActionState.HideToken(true)) - return activeList + disabledList + actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) + return actionsList } - private suspend fun isSendDisabled( - userWalletId: UserWalletId, + private fun getSendUnavailabilityReason( cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, - ): Boolean { - val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, cryptoCurrencyStatus.currency) - val notEnoughBalanceForFee = isNotEnoughBalanceForFee( - feePaidCurrency = feePaidCurrency, - tokenStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) - return cryptoCurrencyStatus.value.amount.isNullOrZero() || - notEnoughBalanceForFee || + ): ScenarioUnavailabilityReason { + return when { + cryptoCurrencyStatus.value.amount.isNullOrZero() -> { + ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) + } currenciesRepository.hasPendingTransactions( cryptoCurrencyStatus = cryptoCurrencyStatus, coinStatus = coinStatus, - ) - } - - private fun isNotEnoughBalanceForFee( - feePaidCurrency: FeePaidCurrency, - tokenStatus: CryptoCurrencyStatus, - coinStatus: CryptoCurrencyStatus?, - ): Boolean { - return if (sendFeatureToggles.isRedesignedSendEnabled) { - tokenStatus.value.amount.isZero() - } else { - when (feePaidCurrency) { - FeePaidCurrency.Coin -> !tokenStatus.value.amount.isZero() && coinStatus?.value?.amount.isZero() - FeePaidCurrency.SameCurrency -> tokenStatus.value.amount.isZero() - is FeePaidCurrency.Token -> { - val feePaidTokenBalance = feePaidCurrency.balance - !tokenStatus.value.amount.isZero() && feePaidTokenBalance.isZero() - } + ) -> { + ScenarioUnavailabilityReason.PendingTransaction( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND, + networkName = coinStatus?.currency?.network?.name.orEmpty(), + ) + } + else -> { + ScenarioUnavailabilityReason.None } } } @@ -218,8 +264,4 @@ class GetCryptoCurrencyActionsUseCase( private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } - - private fun BigDecimal?.isZero(): Boolean { - return this?.signum() == 0 - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 5b3bf0649d..ba46c3aca0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -6,8 +6,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.* +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.api.SwapRepository @@ -76,6 +78,7 @@ class GetCurrencyWarningsUseCase( getNetworkUnavailableWarning(currencyStatus), getNetworkNoAccountWarning(currencyStatus), getBeaconChainShutdownWarning(currency.network.id), + getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), ) }.flowOn(dispatchers.io) } @@ -169,9 +172,6 @@ class GetCurrencyWarningsUseCase( when { tokenStatus != null && coinStatus != null -> { buildList { - if (currenciesRepository.hasPendingTransactions(tokenStatus, coinStatus)) { - add(CryptoCurrencyWarning.HasPendingTransactions(coinStatus.currency.symbol)) - } getFeeWarning( userWalletId = userWalletId, coinStatus = coinStatus, @@ -270,6 +270,22 @@ class GetCurrencyWarningsUseCase( return if (BlockchainUtils.isBeaconChain(networkId.value)) CryptoCurrencyWarning.BeaconChainShutdown else null } + private suspend fun getAssetRequirementsWarning( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): CryptoCurrencyWarning? { + return when (val requirements = walletManagersFacade.getAssetRequirements(userWalletId, currency)) { + is AssetRequirementsCondition.PaidTransaction -> HederaWarnings.AssociateWarning(currency = currency) + is AssetRequirementsCondition.PaidTransactionWithFee -> HederaWarnings.AssociateWarningWithFee( + currency = currency, + fee = requirements.feeAmount, + feeCurrencySymbol = requirements.feeCurrencySymbol, + feeCurrencyDecimals = requirements.decimals, + ) + null -> null + } + } + private fun BigDecimal?.isZero(): Boolean { return this?.signum() == 0 } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/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..54db16bf92 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens.model + +sealed class ScenarioUnavailabilityReason { + data object None : ScenarioUnavailabilityReason() + + // send&sell-specific + data class PendingTransaction( + val withdrawalScenario: WithdrawalScenario, + val networkName: String, + ) : ScenarioUnavailabilityReason() + data class EmptyBalance(val withdrawalScenario: WithdrawalScenario) : ScenarioUnavailabilityReason() + + // buy-specific + data class BuyUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + // swap-specific + data class NotExchangeable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + // sell-specific + data class NotSupportedBySellService(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + + data object Unreachable : ScenarioUnavailabilityReason() + + data object UnassociatedAsset : ScenarioUnavailabilityReason() + + enum class WithdrawalScenario { + SELL, SEND + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index f52641b2c8..372f1e51d5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -10,20 +10,20 @@ data class TokenActionsState( sealed class ActionState { - abstract val enabled: Boolean + abstract val unavailabilityReason: ScenarioUnavailabilityReason - data class Buy(override val enabled: Boolean) : ActionState() + data class Buy(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class CopyAddress(override val enabled: Boolean) : ActionState() + data class CopyAddress(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Sell(override val enabled: Boolean) : ActionState() + data class Sell(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Receive(override val enabled: Boolean) : ActionState() + data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Swap(override val enabled: Boolean) : ActionState() + data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class Send(override val enabled: Boolean) : ActionState() + data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() - data class HideToken(override val enabled: Boolean) : ActionState() + data class HideToken(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt new file mode 100644 index 0000000000..c358cd158e --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -0,0 +1,153 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.* +import arrow.core.raise.recover +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lce +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.* + +internal class CurrenciesStatusesLceOperations( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, +) { + + fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> { + return getMultiCurrencyWalletCurrencies(userWalletId).transform transform@{ maybeCurrencies -> + val nonEmptyCurrencies = maybeCurrencies.fold( + ifLoading = { maybeContent -> + emit(createLoadingCurrenciesStatuses(maybeContent)) + return@transform + }, + ifContent = { content -> + val nonEmptyCurrencies = content.toNonEmptyListOrNull() + + if (nonEmptyCurrencies == null) { + emit(TokenListError.EmptyTokens.lceError()) + return@transform + } else { + nonEmptyCurrencies + } + }, + ifError = { error -> + emit(error.lceError()) + return@transform + }, + ) + + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + + combine( + getQuotes(currenciesIds), + getNetworksStatuses(userWalletId, networks), + ) { maybeQuotes, maybeNetworksStatuses -> + val statuses = createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + emit(statuses) + }.collect() + } + } + + private fun createLoadingCurrenciesStatuses( + maybeCurrencies: List?, + ): Lce> { + val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull() + + val statuses = if (nonEmptyCurrencies == null) { + lceLoading() + } else { + createCurrenciesStatuses( + nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + ) + } + + return statuses + } + + private fun getMultiCurrencyWalletCurrencies( + userWalletId: UserWalletId, + ): LceFlow> { + return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId) + .map { maybeCurrencies -> + maybeCurrencies.mapError { TokenListError.DataError(it) } + } + } + + private fun createCurrenciesStatuses( + currencies: NonEmptyList, + maybeQuotes: Either>?, + maybeNetworkStatuses: Lce>?, + ): Lce> = lce { + isLoading.set(maybeNetworkStatuses == null) + + var quotesRetrievingFailed = false + + val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull() + val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { + quotesRetrievingFailed = true + null + } + + currencies.map { currency -> + val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } + + createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + } + } + + private fun createCurrencyStatus( + currency: CryptoCurrency, + quote: Quote?, + networkStatus: NetworkStatus?, + ignoreQuote: Boolean, + ): CryptoCurrencyStatus { + val currencyStatusOperations = CurrencyStatusOperations( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = ignoreQuote, + ) + + return currencyStatusOperations.createTokenStatus() + } + + private fun getQuotes(tokensIds: NonEmptySet): Flow>> { + return quotesRepository.getQuotesUpdates(tokensIds) + .map, Either>> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } + } + + private fun getNetworksStatuses( + userWalletId: UserWalletId, + networks: NonEmptySet, + ): LceFlow> { + return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks) + .map { maybeStatuses -> + maybeStatuses.mapError { TokenListError.DataError(it) } + } + } + + private fun getIds(currencies: List): Pair, NonEmptySet> { + val currencyIdToNetworkId = currencies.associate { currency -> + currency.id to currency.network + } + val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() + val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() + + requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } + requireNotNull(networks) { "Networks IDs cannot be empty" } + + return networks to currenciesIds + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index d12ca4c9ad..4ae1a1f01c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -2,7 +2,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* -import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -20,18 +20,8 @@ internal class CurrenciesStatusesOperations( private val userWalletId: UserWalletId, ) { - constructor( - userWalletId: UserWalletId, - useCase: GetTokenListUseCase, - ) : this( - currenciesRepository = useCase.currenciesRepository, - quotesRepository = useCase.quotesRepository, - networksRepository = useCase.networksRepository, - userWalletId = userWalletId, - ) - @OptIn(ExperimentalCoroutinesApi::class) - fun getCurrenciesStatusesFlow(): Flow>> { + fun getCurrenciesStatusesFlow(): EitherFlow> { return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> val nonEmptyCurrencies = maybeCurrencies.fold( ifLeft = { error -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index b02828d2c3..8284d1667e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -12,7 +12,7 @@ internal class CurrencyStatusOperations( fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus()) - private fun createStatus(): CryptoCurrencyStatus.Status { + private fun createStatus(): CryptoCurrencyStatus.Value { return when (val status = networkStatus?.value) { null -> CryptoCurrencyStatus.Loading is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() @@ -42,7 +42,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, ) - private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { + private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Value { val amount = when (val amount = status.amounts[currency.id]) { null -> { return CryptoCurrencyStatus.Loading diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 612140fbfb..9c75a10432 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -4,7 +4,6 @@ import arrow.core.* import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.withError -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -18,16 +17,6 @@ internal class TokenListOperations( private val tokens: List, ) { - constructor( - userWalletId: UserWalletId, - tokens: List, - useCase: GetTokenListUseCase, - ) : this( - currenciesRepository = useCase.currenciesRepository, - userWalletId = userWalletId, - tokens = tokens, - ) - fun getTokenListFlow(): Flow> { return combine( getIsGrouped(), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 60f8d5da7e..61cc6f66ee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -1,5 +1,7 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.core.error.DataError +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -20,7 +22,7 @@ interface CurrenciesRepository { * @param currencies The list of cryptocurrencies to be saved. * @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network. * @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun saveTokens( @@ -35,7 +37,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param currencies The currencies which must be added. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) @@ -45,7 +47,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param currency The currency which must be removed. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) @@ -55,7 +57,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @param currencies The currencies which must be removed. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) @@ -65,7 +67,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return The primary cryptocurrency associated with the user wallet. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency @@ -75,7 +77,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return The primary cryptocurrency associated with the user wallet. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List @@ -87,7 +89,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @param id The unique identifier of the cryptocurrency to be retrieved. * @return The cryptocurrency associated with the user wallet and ID. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun getSingleCurrencyWalletWithCardCurrency( @@ -102,11 +104,22 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> + /** + * Retrieves updates of the list of cryptocurrencies within a multi-currency wallet. + * + * Loads remote cryptocurrencies if they have expired. + * + * @param userWalletId The unique identifier of the user wallet. + * @return A [LceFlow] emitting the set of cryptocurrencies associated with the user wallet. May emit an + * [DataError.UserWalletError.WrongUserWallet] if single-currency user wallet ID provided. + */ + fun getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId: UserWalletId): LceFlow> + /** * Retrieves the list of cryptocurrencies within a multi-currency wallet. * @@ -115,7 +128,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A list of [CryptoCurrency]. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun getMultiCurrencyWalletCurrenciesSync( @@ -129,7 +142,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @param id The unique identifier of the cryptocurrency to be retrieved. * @return The cryptocurrency associated with the user wallet and ID. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency @@ -152,7 +165,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting a boolean value indicating whether the tokens are grouped. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ fun isTokensGrouped(userWalletId: UserWalletId): Flow @@ -162,7 +175,7 @@ interface CurrenciesRepository { * * @param userWalletId The unique identifier of the user wallet. * @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance. - * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 271cdb4fcf..2603914a46 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.repository +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus @@ -21,6 +22,20 @@ interface NetworksRepository { */ fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> + /** + * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. + * + * Loads remote network statuses if they have expired. + * + * @param userWalletId The unique identifier of the user wallet. + * @param networks A set of network which statuses are to be retrieved. + * @return A [LceFlow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. + */ + fun getNetworkStatusesUpdatesLce( + userWalletId: UserWalletId, + networks: Set, + ): LceFlow> + /** * Fetches pending transactions for given network * 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 34abccfa04..78e06e7087 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -4,6 +4,8 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId @@ -22,6 +24,13 @@ internal class MockNetworksRepository( return statuses.map { it.getOrElse { e -> throw e } } } + override fun getNetworkStatusesUpdatesLce( + userWalletId: UserWalletId, + networks: Set, + ): LceFlow> { + return statuses.map { it.toLce() } + } + override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { // no-op } diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index f0345df888..d4014c6f47 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/domain/transaction/models/.gitignore b/domain/transaction/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/transaction/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/transaction/models/build.gradle.kts b/domain/transaction/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/transaction/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt new file mode 100644 index 0000000000..3503f354df --- /dev/null +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/AssetRequirementsCondition.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.transaction.models + +import java.math.BigDecimal + +sealed class AssetRequirementsCondition { + + /** + * The exact value of the fee for this type of condition is unknown. + */ + data object PaidTransaction : AssetRequirementsCondition() + + /** + * The exact value of the fee for this type of condition is stored in `feeAmount`. + */ + data class PaidTransactionWithFee( + val feeAmount: BigDecimal, + val feeCurrencySymbol: String, + val decimals: Int, + ) : AssetRequirementsCondition() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 70c8b7d0ae..156e2bb0fa 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -18,8 +18,22 @@ interface TransactionRepository { destination: String, userWalletId: UserWalletId, network: Network, + isSwap: Boolean, + hash: String?, ): TransactionData? + @Suppress("LongParameterList") + suspend fun validateTransaction( + amount: Amount, + fee: Fee?, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean = false, + hash: String? = null, + ): Result + suspend fun sendTransaction( txData: TransactionData, signer: CommonSigner, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt new file mode 100644 index 0000000000..fc92dd8cea --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/AssociateAssetError.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.transaction.error + +import com.tangem.domain.tokens.model.CryptoCurrency + +sealed class AssociateAssetError { + data class NotEnoughBalance(val feeCurrency: CryptoCurrency) : AssociateAssetError() + + data class DataError(val message: String?) : AssociateAssetError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt new file mode 100644 index 0000000000..3187c90352 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.transaction.error.AssociateAssetError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.isNullOrZero + +class AssociateAssetUseCase( + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Either { + return either { + val networkCoin = currenciesRepository.getNetworkCoin( + userWalletId = userWalletId, + networkId = currency.network.id, + derivationPath = currency.network.derivationPath, + ) + if (isBalanceZero(userWalletId, networkCoin)) { + raise(AssociateAssetError.NotEnoughBalance(networkCoin)) + } + val signer = cardSdkConfigRepository.getCommonSigner(cardId = null) + + catch( + block = { + when (val result = walletManagersFacade.associateAsset(userWalletId, currency, signer)) { + is SimpleResult.Failure -> raise(AssociateAssetError.DataError(result.error.message)) + SimpleResult.Success -> Unit + } + }, + catch = { error -> AssociateAssetError.DataError(error.message) }, + ) + } + } + + private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { + val networkStatus = networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = setOf(currency.network), + ).find { it.network == currency.network } + val networkCoinAmountStatus = (networkStatus?.value as? NetworkStatus.Verified) + ?.amounts + ?.get(currency.id) + return networkCoinAmountStatus is CryptoCurrencyAmountStatus.Loaded && + networkCoinAmountStatus.value.isNullOrZero() + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt index 55c5e0cd88..9fea02c12f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionUseCase.kt @@ -22,6 +22,8 @@ class CreateTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, + isSwap: Boolean = false, + hash: String? = null, ) = Either.catch { requireNotNull( transactionRepository.createTransaction( @@ -31,6 +33,8 @@ class CreateTransactionUseCase( destination = destination, userWalletId = userWalletId, network = network, + isSwap = isSwap, + hash = hash, ), ) { "Failed to create transaction" } } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt new file mode 100644 index 0000000000..56c89954a4 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId + +class ValidateTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + + @Suppress("LongParameterList") + suspend operator fun invoke( + amount: Amount, + fee: Fee, + memo: String?, + destination: String, + userWalletId: UserWalletId, + network: Network, + isSwap: Boolean = false, + hash: String? = null, + ): Either { + return transactionRepository.validateTransaction( + amount = amount, + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWalletId, + network = network, + isSwap = isSwap, + hash = hash, + ) + .fold(onSuccess = { Unit.right() }, onFailure = { it.left() }) + } +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 0f3adc92e4..bd175a01fa 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { // region Domain modules implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt similarity index 69% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt index 325fb2efa7..ffa69e47ae 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt @@ -1,36 +1,22 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.builder import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase class UserWalletBuilder( private val scanResponse: ScanResponse, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(), ) { private var backupCardsIds: Set = emptySet() private var hasBackupError: Boolean = false private val CardDTO.isBackupNotAllowed: Boolean - get() = !this.settings.isBackupAllowed - - private val ScanResponse.userWalletName: String - get() = when (productType) { - ProductType.Note -> "Note" - ProductType.Twins -> "Twin" - ProductType.Start2Coin -> "Start2Coin" - ProductType.Visa -> "Tangem Visa" - ProductType.Wallet, - ProductType.Wallet2, - ProductType.Ring, - -> when { - card.isBackupNotAllowed -> "Tangem card" - cardTypesResolver.isStart2Coin() -> "Start2Coin" - else -> "Wallet" - } - } + get() = !settings.isBackupAllowed /** * DANGEROUS!!! @@ -56,7 +42,11 @@ class UserWalletBuilder( ?.let { UserWallet( walletId = it, - name = userWalletName, + name = generateWalletNameUseCase( + productType = productType, + isBackupNotAllowed = card.isBackupNotAllowed, + isStartToCoin = cardTypesResolver.isStart2Coin(), + ), artworkUrl = getCardImageUseCase.invoke(card.cardId, card.cardPublicKey), cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletIdBuilder.kt similarity index 98% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletIdBuilder.kt index 607484d8a8..7c916ecec1 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletIdBuilder.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.builder import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.hexToBytes diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UseCaseUtils.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UseCaseUtils.kt deleted file mode 100644 index ee7de756db..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UseCaseUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import arrow.core.raise.Raise -import arrow.core.raise.ensureNotNull - -internal inline fun Raise.ensureUserWalletListManagerNotNull( - walletsStateHolder: WalletsStateHolder, - raise: (Throwable) -> Error, -): UserWalletsListManager { - return ensureNotNull( - value = walletsStateHolder.userWalletsListManager, - raise = { - raise(IllegalStateException("User wallets list manager not initialized")) - }, - ) -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt index 213d780d07..9b63847e73 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt @@ -7,12 +7,20 @@ import kotlinx.coroutines.flow.Flow interface UserWalletsListManager { + /** + * Indicates that the [UserWalletsListManager] is [UserWalletsListManager.Lockable] + * */ + val isLockable: Boolean + /** [Flow] with all saved [UserWallet]s updates */ val userWallets: Flow> /** [Flow] with selected [UserWallet] updates */ val selectedUserWallet: Flow + /** [List] with all saved [UserWallet]s updates */ + val userWalletsSync: List + /** Selected [UserWallet] */ val selectedUserWalletSync: UserWallet? @@ -84,11 +92,6 @@ interface UserWalletsListManager { */ suspend fun get(userWalletId: UserWalletId): CompletionResult - /** - * Indicates that the [UserWalletsListManager] supports [UserWalletsListManager.Lockable] - * */ - fun isLockable(): Boolean - interface Lockable : UserWalletsListManager { /** diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt index a6bdbde07f..9570eaa90d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt @@ -49,7 +49,7 @@ suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockTyp * [UserWalletsListManager.Lockable] otherwise * */ fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? { - if (this.isLockable()) { + if (this.isLockable) { return this as? UserWalletsListManager.Lockable } return null diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt deleted file mode 100644 index d5af7e4449..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerFeatureToggles.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.wallets.legacy - -interface UserWalletsListManagerFeatureToggles { - - val isGeneralManagerEnabled: Boolean -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/WalletsStateHolder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/WalletsStateHolder.kt deleted file mode 100644 index 7aa875c6af..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/WalletsStateHolder.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import kotlinx.coroutines.flow.Flow - -interface WalletsStateHolder { - - val userWalletsListManager: UserWalletsListManager? - - val userWalletListManagerFlow: Flow -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/Artwork.kt similarity index 66% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/models/Artwork.kt index d788aed532..00b7961d48 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/Artwork.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.models data class Artwork(val artworkId: String) { @@ -6,9 +6,9 @@ data class Artwork(val artworkId: String) { const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png" const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png" const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png" + const val TWIN_CARD_1_URL = "https://app.tangem.com/cards/card_tg085.png" + const val TWIN_CARD_2_URL = "https://app.tangem.com/cards/card_tg086.png" const val SERGIO_CARD_ID = "BC01" const val MARTA_CARD_ID = "BC02" - const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png" - const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png" } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt index 29669fa19e..d58c220ac3 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt @@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models sealed interface DeleteWalletError { - object DataError : DeleteWalletError - - object UnableToDelete : DeleteWalletError + data object UnableToDelete : DeleteWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt index 50d1bea262..3e7e62a562 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/GetUserWalletError.kt @@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models sealed class GetUserWalletError { - data class DataError(val cause: Throwable) : GetUserWalletError() - - object UserWalletNotFound : GetUserWalletError() + data object UserWalletNotFound : GetUserWalletError() } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt index a357f06ca9..e2aeab608f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt @@ -2,7 +2,5 @@ package com.tangem.domain.wallets.models sealed interface SelectWalletError { - object DataError : SelectWalletError - object UnableToSelectUserWallet : SelectWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt index 48a93dd33b..b15ec332d1 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt @@ -2,5 +2,7 @@ package com.tangem.domain.wallets.models sealed interface UpdateWalletError { - object DataError : UpdateWalletError + data object DataError : UpdateWalletError + + data object NameAlreadyExists : UpdateWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletNamesMigrationRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletNamesMigrationRepository.kt new file mode 100644 index 0000000000..0fa502f042 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletNamesMigrationRepository.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.wallets.repository + +/** + * Access to migrate names flag + */ +interface WalletNamesMigrationRepository { + + suspend fun isMigrationDone(): Boolean + + suspend fun setMigrationDone() +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index 2b43096bf4..b4db11932f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -1,37 +1,36 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.left import arrow.core.raise.either -import arrow.core.right import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.DeleteWalletError import com.tangem.domain.wallets.models.UserWalletId /** - * Use case for updating user wallet + * Use case for deleting user wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class DeleteWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { - suspend operator fun invoke(userWalletId: UserWalletId): Either { + /** + * Deletes user wallet with provided ID. + * + * @param userWalletId ID of user wallet to be deleted. + * + * @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets. + * */ + suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { DeleteWalletError.DataError }, - ) - userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) - .doOnSuccess { return Unit.right() } - .doOnFailure { return DeleteWalletError.UnableToDelete.left() } + .doOnFailure { + raise(DeleteWalletError.UnableToDelete) + } - return Unit.right() + userWalletsListManager.hasUserWallets } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt new file mode 100644 index 0000000000..f1d1852f0c --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.wallets.legacy.UserWalletsListManager + +/** + * Use case for user wallet name generation + */ +class GenerateWalletNameUseCase( + private val userWalletsListManager: UserWalletsListManager, +) { + + operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { + val defaultName = getDefaultName( + productType = productType, + isBackupNotAllowed = isBackupNotAllowed, + isStartToCoin = isStartToCoin, + ) + + val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + return suggestedWalletName(defaultName, existingNames) + } + + private fun suggestedWalletName(defaultName: String, existingNames: Set): String { + val startIndex = 2 + if (!existingNames.contains(defaultName)) { + return defaultName + } + + for (index in startIndex..MAX_WALLETS_LIMIT) { + val potentialName = "$defaultName $index" + if (!existingNames.contains(potentialName)) { + return potentialName + } + } + + return defaultName + } + + private fun getDefaultName(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { + return when (productType) { + ProductType.Note -> "Note" + ProductType.Twins -> "Twin" + ProductType.Start2Coin -> "Start2Coin" + ProductType.Visa -> "Tangem Visa" + ProductType.Wallet, + ProductType.Wallet2, + ProductType.Ring, + -> when { + isBackupNotAllowed -> "Tangem card" + isStartToCoin -> "Start2Coin" + else -> "Wallet" + } + } + } + + companion object { + const val MAX_WALLETS_LIMIT = 10000 + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt similarity index 89% rename from domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index afa5eaaa37..3d92875a5b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -1,9 +1,10 @@ -package com.tangem.domain.userwallets +package com.tangem.domain.wallets.usecase import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper +import com.tangem.domain.wallets.models.Artwork import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.attestation.TangemApi @@ -42,8 +43,8 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL else -> when (TwinsHelper.getTwinCardNumber(cardId)) { - TwinCardNumber.First -> Artwork.TWIN_CARD_1 - TwinCardNumber.Second -> Artwork.TWIN_CARD_2 + TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL + TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL else -> Artwork.DEFAULT_IMG_URL } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt index 7cd6ffa8a2..e9f51327a6 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt @@ -3,8 +3,7 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensureNotNull -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet @@ -12,19 +11,14 @@ import com.tangem.domain.wallets.models.UserWallet * Use case for getting selected wallet. * Important! If all wallets is locked, use case returns a error. * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class GetSelectedWalletSyncUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) { operator fun invoke(): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = GetUserWalletError::DataError, - ) - ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, raise = { GetUserWalletError.UserWalletNotFound }, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index ee23723206..bda3c953ee 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -2,8 +2,7 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow @@ -11,19 +10,14 @@ import kotlinx.coroutines.flow.Flow /** * Use case for getting flow of selected wallet. * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class GetSelectedWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { operator fun invoke(): Either> { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = GetUserWalletError::DataError, - ) - userWalletsListManager.selectedUserWallet } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index 2b9c40ae27..9323fd5423 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -3,22 +3,15 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensureNotNull -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.firstOrNull -class GetUserWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { - suspend operator fun invoke(userWalletId: UserWalletId): Either = either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = GetUserWalletError::DataError, - ) - - val userWallets = userWalletsListManager.userWallets.firstOrNull().orEmpty() + operator fun invoke(userWalletId: UserWalletId): Either = either { + val userWallets = userWalletsListManager.userWalletsSync ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { raise(GetUserWalletError.UserWalletNotFound) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt new file mode 100644 index 0000000000..0108e03b67 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.wallets.legacy.UserWalletsListManager + +/** + * Use case for getting list of user wallets names. + * + * @property userWalletsListManager user wallets list manager + */ +class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) { + + operator fun invoke(): List = userWalletsListManager.userWalletsSync.map { it.name } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index e40e284270..4045a3a98a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -1,6 +1,6 @@ package com.tangem.domain.wallets.usecase -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull @@ -8,16 +8,15 @@ import kotlinx.coroutines.flow.firstOrNull /** * Use case for getting list of user wallets * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class GetWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { +class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { @Throws(IllegalArgumentException::class) - operator fun invoke(): Flow> = - requireNotNull(walletsStateHolder.userWalletsListManager).userWallets + operator fun invoke(): Flow> = userWalletsListManager.userWallets @Throws(IllegalArgumentException::class) - suspend fun invokeSync(): List? = walletsStateHolder.userWalletsListManager?.userWallets?.firstOrNull() + suspend fun invokeSync(): List? = userWalletsListManager.userWallets.firstOrNull() } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt index 24fdc5a232..e403101dd5 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt @@ -1,19 +1,19 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map /** * Use case that checks if wallet need backup cards + * + * @property userWalletsListManager user wallets list manager */ -class IsNeedToBackupUseCase(private val walletsStateHolder: WalletsStateHolder) { +class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) { operator fun invoke(id: UserWalletId): Flow { - val userWalletsListManager = requireNotNull(walletsStateHolder.userWalletsListManager) - return userWalletsListManager.userWallets .map { wallets -> val wallet = wallets.firstOrNull { it.walletId == id } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt new file mode 100644 index 0000000000..e03da26f6b --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for rename user wallet + * + * @property userWalletsListManager user wallets list manager + */ +class RenameWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { + + suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either { + val existingNames = userWalletsListManager.userWalletsSync + + if (existingNames.any { it.name == name && it.walletId != userWalletId }) { + return UpdateWalletError.NameAlreadyExists.left() + } + + return either { + userWalletsListManager.update(userWalletId) { it.copy(name = name) } + .doOnSuccess { return it.right() } + .doOnFailure { return UpdateWalletError.DataError.left() } + + return UpdateWalletError.DataError.left() + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 8e25c40c1e..0f3becefc0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -7,27 +7,21 @@ import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.SaveWalletError import com.tangem.domain.wallets.models.UserWallet /** * Use case for saving user wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { SaveWalletError.DataError }, - ) - userWalletsListManager.save(userWallet, canOverride) .doOnSuccess { return Unit.right() } .doOnFailure { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 84303de548..da446a2a40 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -5,8 +5,7 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.CompletionResult import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.SelectWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -14,22 +13,18 @@ import com.tangem.domain.wallets.models.UserWalletId /** * Use case for selecting wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager + * @property reduxStateHolder redux state holder * [REDACTED_AUTHOR] */ class SelectWalletUseCase( - private val walletsStateHolder: WalletsStateHolder, + private val userWalletsListManager: UserWalletsListManager, private val reduxStateHolder: ReduxStateHolder, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { SelectWalletError.DataError }, - ) - return when (val result = userWalletsListManager.select(userWalletId)) { is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) is CompletionResult.Success -> { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt index 9b6bdf4997..4d46212807 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt @@ -5,23 +5,23 @@ import arrow.core.raise.either import arrow.core.raise.ensureNotNull import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListError +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType -import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UnlockWalletsError /** * Unlock wallets use case * - * @property walletsStateHolder wallets state holder + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class UnlockWalletsUseCase(private val walletsStateHolder: WalletsStateHolder) { +class UnlockWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either = either { val userWalletsListManager = ensureNotNull( - value = walletsStateHolder.userWalletsListManager?.asLockable(), + value = userWalletsListManager.asLockable(), raise = { UnlockWalletsError.DataError( cause = IllegalStateException("The lockable user wallets list manager could not be found"), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index 3a97779a39..b060418b2e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -6,8 +6,7 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.legacy.ensureUserWalletListManagerNotNull +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -15,22 +14,17 @@ import com.tangem.domain.wallets.models.UserWalletId /** * Use case for updating user wallet * - * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * @property userWalletsListManager user wallets list manager * [REDACTED_AUTHOR] */ -class UpdateWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { +class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, ): Either { return either { - val userWalletsListManager = ensureUserWalletListManagerNotNull( - walletsStateHolder = walletsStateHolder, - raise = { UpdateWalletError.DataError }, - ) - userWalletsListManager.update(userWalletId, update) .doOnSuccess { return it.right() } .doOnFailure { return UpdateWalletError.DataError.left() } diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 8fb983d6d7..88d462b52c 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt index d335aaca71..d5691e9017 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.addcustomtoken.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -24,6 +25,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.AlertState import com.tangem.managetokens.presentation.common.state.ChooseWalletState @@ -287,17 +289,10 @@ private fun TokenTextFieldTitle(state: TextFieldState?, title: String) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseDerivationScreen_Light() { - TangemTheme(isDark = false) { - AddCustomTokenScreen(state = AddCustomTokenPreviewData.state) - } -} - -@Preview -@Composable -private fun Preview_ChooseDerivationScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseDerivationScreen() { + TangemThemePreview { AddCustomTokenScreen(state = AddCustomTokenPreviewData.state) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt index 3422e1cf8e..a7805cbb96 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.addcustomtoken.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,6 +14,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock @@ -98,17 +100,10 @@ private fun DerivationsList(state: ChooseDerivationState) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseDerivationScreen_Light() { - TangemTheme(isDark = false) { - ChooseDerivationScreen(state = ChooseDerivationPreviewData.state) - } -} - -@Preview -@Composable -private fun Preview_ChooseDerivationScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseDerivationScreen() { + TangemThemePreview { ChooseDerivationScreen(state = ChooseDerivationPreviewData.state) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt index 4ee903652e..6777fc4fbc 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.addcustomtoken.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,6 +14,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.ui.components.NetworkItem @@ -78,17 +80,10 @@ internal fun ChooseNetworkCustomScreen(state: ChooseNetworkState, modifier: Modi } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseNetworkScreen_Light() { - TangemTheme(isDark = false) { - ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state) - } -} - -@Preview -@Composable -private fun Preview_ChooseNetworkScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseNetworkScreen() { + TangemThemePreview { ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt index 878a5f28e1..121faaba26 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.common.ui +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,6 +24,7 @@ import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.ChooseWalletState import com.tangem.managetokens.presentation.common.state.WalletState @@ -137,19 +139,10 @@ private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifi } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseWalletScreen_Light() { - TangemTheme(isDark = false) { - ChooseWalletScreen( - state = ChooseWalletStatePreviewData.state, - ) - } -} - -@Preview -@Composable -private fun Preview_ChooseWalletScreen_Dark() { - TangemTheme(isDark = false) { +private fun Preview_ChooseWalletScreen() { + TangemThemePreview { ChooseWalletScreen( state = ChooseWalletStatePreviewData.state, ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt index 0359b28aa1..553a6bd999 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.common.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.NetworkItemState @@ -122,17 +124,10 @@ internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NetworkItem_Light(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { - TangemTheme(isDark = false) { - NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) - } -} - -@Preview -@Composable -private fun Preview_NetworkItem_Dark(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { - TangemTheme(isDark = true) { +private fun Preview_NetworkItem(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { + TangemThemePreview { NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt index c136cb1e28..e7ad5cf31b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.common.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -12,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @Composable @@ -54,17 +56,10 @@ fun SimpleSelectionBlock( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_SimpleSelectionBlock_Light() { - TangemTheme(isDark = false) { - SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) - } -} - -@Preview -@Composable -private fun Preview_SimpleSelectionBlock_Dark() { - TangemTheme(isDark = true) { +private fun Preview_SimpleSelectionBlock() { + TangemThemePreview { SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt index ead0700da2..2f5d51df8a 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,6 +18,7 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.WarningCardTitleOnly import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.common.state.ChooseWalletState @@ -187,20 +189,10 @@ private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ChooseNetworkScreen_Light() { - TangemTheme(isDark = false) { - ChooseNetworkScreen( - state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, - walletState = ChooseWalletStatePreviewData.state, - ) - } -} - -@Preview -@Composable -private fun Preview_ChooseNetworkScreen_Dark() { - TangemTheme(isDark = true) { +private fun Preview_ChooseNetworkScreen() { + TangemThemePreview { ChooseNetworkScreen( state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, walletState = ChooseWalletStatePreviewData.state, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt index 12f7a0a191..15197a8e4a 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui +import android.content.res.Configuration import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH18 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenBottomSheet import com.tangem.managetokens.presentation.common.state.AlertState @@ -177,23 +179,13 @@ private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ManageTokensScreen_LightTheme( +private fun Preview_ManageTokensScreen( @PreviewParameter(ManageTokensConfigProvider::class) state: ManageTokensState, ) { - TangemTheme(isDark = false) { - ManageTokensScreen(state) {} - } -} - -@Preview -@Composable -private fun Preview_ManageTokensScreen_DarkTheme( - @PreviewParameter(ManageTokensConfigProvider::class) - state: ManageTokensState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ManageTokensScreen(state) {} } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt index 195bb4d60c..32962c70db 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -14,6 +15,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.impl.R @Composable @@ -49,17 +51,10 @@ internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AddCustomTokenButton_Preview_Light() { - TangemTheme(isDark = false) { - AddCustomTokenButton(onButtonClick = { }) - } -} - -@Preview -@Composable -private fun AddCustomTokenButton_Preview_Dark() { - TangemTheme(isDark = true) { +private fun AddCustomTokenButton_Preview() { + TangemThemePreview { AddCustomTokenButton(onButtonClick = { }) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt index a18fffaf56..d1f3822c9b 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -18,6 +19,7 @@ import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.managetokens.state.previewdata.DerivationNotificationStatePreviewData @@ -127,17 +129,10 @@ private fun TextsBlock(title: TextReference, subtitle: TextReference) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ManageTokensScreen_LightTheme() { - TangemTheme(isDark = false) { - DerivationNotification(DerivationNotificationStatePreviewData.state.config) - } -} - -@Preview -@Composable -private fun Preview_ManageTokensScreen_DarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_ManageTokensScreen() { + TangemThemePreview { DerivationNotification(DerivationNotificationStatePreviewData.state.config) } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt index 998dbebc72..1a18c5ee93 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -105,7 +106,7 @@ private fun getValuePercentageForRange(value: Float, max: Float, min: Float): Fl @Preview(widthDp = 150, heightDp = 150, showBackground = true) @Composable private fun Chart_Positive_Preview() { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { PriceChangesChart( persistentListOf(1f, 2f, 4f, 1f, 5f), ) @@ -115,7 +116,7 @@ private fun Chart_Positive_Preview() { @Preview(widthDp = 150, heightDp = 150, showBackground = true) @Composable private fun Chart_Negative_Preview() { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { PriceChangesChart( persistentListOf(10f, 2f, 4f, 1f, 5f), ) @@ -125,7 +126,7 @@ private fun Chart_Negative_Preview() { @Preview(widthDp = 150, heightDp = 150, showBackground = true) @Composable private fun Chart_Neutral_Preview() { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { PriceChangesChart( persistentListOf(5f, 2f, 4f, 1f, 5f), ) diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt index 6040c96260..8faf911ebb 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape @@ -20,6 +21,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.managetokens.state.SearchBarState @@ -107,20 +109,13 @@ private fun searchbarTextFieldColors(): TextFieldColors { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokensSearchBar_Light( +private fun Preview_TokensSearchBar( @PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarState, ) { - TangemTheme(isDark = false) { - TokensSearchBar(state) - } -} - -@Preview -@Composable -private fun Preview_TokensSearchBar_Dark(@PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarState) { - TangemTheme(isDark = true) { + TangemThemePreview { TokensSearchBar(state) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt index f3f76effd3..3829ec91db 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -18,6 +19,7 @@ import com.tangem.core.ui.components.buttons.PrimarySmallButton import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.managetokens.impl.R import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType @@ -66,17 +68,10 @@ internal fun TokenButton(type: TokenButtonType, onClick: () -> Unit, modifier: M } @Preview(backgroundColor = 0xffffff, showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TokenButton_Light_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { - TangemTheme(isDark = false) { - TokenButton(type = type, {}) - } -} - -@Preview(showBackground = true) -@Composable -private fun TokenButton_Dark_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { - TangemTheme(isDark = true) { +private fun TokenButton_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { + TangemThemePreview { TokenButton(type = type, {}) } } diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt index d92f0a8b81..50cf504643 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt @@ -1,5 +1,6 @@ package com.tangem.managetokens.presentation.managetokens.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Surface @@ -14,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.managetokens.presentation.managetokens.state.QuotesState import com.tangem.managetokens.presentation.managetokens.state.TokenItemState @@ -182,17 +184,10 @@ private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () - // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemTheme(isDark = false) { - TokenRowItem(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun Preview_Tokens_DarkTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemTheme(isDark = true) { +private fun Preview_Tokens(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { + TangemThemePreview { TokenRowItem(state) } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index abc2bfff70..de2ffddcbc 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -21,6 +22,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState @@ -204,11 +206,12 @@ private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: D } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SuggestionsBlockPreview_Light( +private fun SuggestionsBlockPreview( @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, ) { - TangemTheme(isDark = false) { + TangemThemePreview { SuggestionsBlock( suggestionsList = suggestions, onClick = {}, @@ -217,24 +220,12 @@ private fun SuggestionsBlockPreview_Light( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SuggestionsBlockPreview_Dark( +private fun ImportSeedPhraseScreenPreview( @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, ) { - TangemTheme(isDark = true) { - SuggestionsBlock( - suggestionsList = suggestions, - onClick = {}, - ) - } -} - -@Preview -@Composable -private fun ImportSeedPhraseScreenPreview_Light( - @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, -) { - TangemTheme(isDark = false) { + TangemThemePreview { Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { ImportSeedPhraseScreen( ImportSeedPhraseState( diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt index ee04bfe228..9228432a75 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent @@ -83,7 +84,7 @@ fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetConte @Preview @Composable private fun PassphraseInfoBottomSheetContentPreview() { - TangemTheme { + TangemThemePreview { PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { }) } } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt index 609e2df784..9fafacb32a 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -16,6 +17,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.* import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText @@ -164,9 +166,10 @@ private inline fun VerticalGrid( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun YourSeedPhraseScreenPreview_Light() { - TangemTheme(isDark = false) { + TangemThemePreview { YourSeedPhraseScreen( state = YourSeedPhraseState( segmentSeedState = SegmentSeedState( diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index f232473da8..72d725c460 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -20,9 +20,9 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.mlkit.vision.common.InputImage +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningContent @@ -39,7 +39,7 @@ import kotlin.properties.Delegates internal class QrScanningFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var router: QrScanningRouter diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 5125499148..5ca72579ad 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 2496fde6e8..2d44c137ff 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -2,12 +2,13 @@ package com.tangem.feature.referral.data import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody import com.tangem.datasource.demo.DemoModeDatasource import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -15,7 +16,6 @@ import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData -import com.tangem.lib.auth.AuthProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import javax.inject.Inject diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt index 2214c244dd..23164dbe82 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -1,12 +1,12 @@ package com.tangem.feature.referral.di +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.demo.DemoModeDatasource import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.data.ReferralRepositoryImpl import com.tangem.feature.referral.domain.ReferralRepository -import com.tangem.lib.auth.AuthProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index e7a122fb45..2ccd4b74fc 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -5,10 +5,10 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.referral.router.ReferralRouter import com.tangem.feature.referral.ui.ReferralScreen import com.tangem.feature.referral.viewmodels.ReferralViewModel @@ -20,7 +20,7 @@ import javax.inject.Inject class ReferralFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies private val viewModel by viewModels() diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index bc175c5bff..e44e192d0d 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -1,5 +1,6 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -16,6 +17,7 @@ import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.referral.presentation.R @@ -58,17 +60,10 @@ private fun AgreementHtmlView(url: String) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AgreementBottomSheet_InLightTheme() { - TangemTheme(isDark = false) { - AgreementBottomSheetContent(url = "https://tangem.com/en/") - } -} - -@Preview -@Composable -private fun Preview_AgreementBottomSheet_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AgreementBottomSheet() { + TangemThemePreview { AgreementBottomSheetContent(url = "https://tangem.com/en/") } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt index d29a6c996b..e005f91c7b 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -1,5 +1,6 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -15,6 +16,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.referral.presentation.R @@ -53,19 +55,10 @@ private fun annotatedAgreementString(firstPart: String): AnnotatedString { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AgreementText_InLightTheme() { - TangemTheme(isDark = false) { - Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { - AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = {}) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_AgreementText_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AgreementText() { + TangemThemePreview { Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = {}) } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt index a85c1a0f07..5f11c074e1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview @Suppress("LongParameterList") @Composable @@ -56,8 +57,8 @@ internal fun AwardText( @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_AwardItem_Light() { - TangemTheme { +private fun Preview_AwardItem() { + TangemThemePreview { AwardText( startText = "startText", startTextColor = TangemTheme.colors.text.tertiary, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt index 78a8239dc0..077fb745d0 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt @@ -1,14 +1,17 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryEndIconButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.presentation.R @Composable @@ -18,29 +21,23 @@ internal fun NonParticipateBottomBlock(onAgreementClick: () -> Unit, onParticipa firstPartResId = R.string.referral_tos_not_enroled_prefix, onClick = onAgreementClick, ) - PrimaryEndIconButton( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + + PrimaryButtonIconEnd( text = stringResource(id = R.string.referral_button_participate), iconResId = R.drawable.ic_tangem_24, onClick = onParticipateClick, + modifier = Modifier + .fillMaxWidth() + .padding(all = TangemTheme.dimens.spacing16), ) } } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NonParticipateBottomBlock_InLightTheme() { - TangemTheme(isDark = false) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { - NonParticipateBottomBlock(onAgreementClick = {}, onParticipateClick = {}) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_NonParticipateBottomBlock_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_NonParticipateBottomBlock() { + TangemThemePreview { Column(Modifier.background(TangemTheme.colors.background.primary)) { NonParticipateBottomBlock(onAgreementClick = {}, onParticipateClick = {}) } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 3417b6931f..39257e4570 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -2,19 +2,14 @@ package com.tangem.feature.referral.ui import android.content.Context import android.content.Intent +import android.content.res.Configuration import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -30,11 +25,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.core.content.ContextCompat.startActivity -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.presentation.R +import kotlinx.coroutines.launch @Suppress("LongParameterList") @Composable @@ -43,8 +40,8 @@ internal fun ParticipateBottomBlock( code: String, shareLink: String, expectedAwards: ExpectedAwards?, + snackbarHostState: SnackbarHostState, onAgreementClick: () -> Unit, - onShowCopySnackbar: () -> Unit, onCopyClick: () -> Unit, onShareClick: () -> Unit, ) { @@ -61,7 +58,7 @@ internal fun ParticipateBottomBlock( AdditionalButtons( code = code, shareLink = shareLink, - onShowCopySnackbar = onShowCopySnackbar, + snackbarHostState = snackbarHostState, onCopyClick = onCopyClick, onShareClick = onShareClick, ) @@ -110,9 +107,9 @@ private fun Awards(expectedAwards: ExpectedAwards) { val elementsCountToShowInLessMode = 3 val isExpanded = remember { mutableStateOf(false) } - Divider( - color = TangemTheme.colors.stroke.primary, + HorizontalDivider( thickness = TangemTheme.dimens.size0_5, + color = TangemTheme.colors.stroke.primary, ) AwardText( startText = if (expectedAwards.expectedAwards.isNotEmpty()) { @@ -270,32 +267,40 @@ private fun PersonalCodeCard(code: String) { private fun AdditionalButtons( code: String, shareLink: String, - onShowCopySnackbar: () -> Unit, + snackbarHostState: SnackbarHostState, onCopyClick: () -> Unit, onShareClick: () -> Unit, ) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + val resources = LocalContext.current.resources + Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { - PrimaryStartIconButton( - modifier = Modifier.weight(1f), + PrimaryButtonIconStart( text = stringResource(id = R.string.common_copy), iconResId = R.drawable.ic_copy_24, onClick = { onCopyClick.invoke() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) clipboardManager.setText(AnnotatedString(code)) - onShowCopySnackbar() + + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getString(R.string.referral_promo_code_copied), + duration = SnackbarDuration.Short, + ) + } }, + modifier = Modifier.weight(1f), ) val context = LocalContext.current - PrimaryStartIconButton( - modifier = Modifier.weight(1f), + PrimaryButtonIconStart( text = stringResource(id = R.string.common_share), iconResId = R.drawable.ic_share_24, onClick = { @@ -303,6 +308,7 @@ private fun AdditionalButtons( hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) context.shareText(context.getString(R.string.referral_share_link, shareLink)) }, + modifier = Modifier.weight(1f), ) } } @@ -318,11 +324,12 @@ private fun Context.shareText(text: String) { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ParticipateBottomBlockPreview_Light( +private fun ParticipateBottomBlockPreview( @PreviewParameter(ParticipateBottomBlockDataProvider::class) data: ParticipateBottomBlockData, ) { - TangemTheme(isDark = false) { + TangemThemePreview { Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = data.purchasedWalletCount, @@ -330,7 +337,7 @@ private fun ParticipateBottomBlockPreview_Light( shareLink = data.shareLink, expectedAwards = data.expectedAwards, onAgreementClick = data.onAgreementClick, - onShowCopySnackbar = data.onShowCopySnackbar, + snackbarHostState = SnackbarHostState(), onCopyClick = data.onCopyClick, onShareClick = data.onShareClick, ) @@ -339,42 +346,10 @@ private fun ParticipateBottomBlockPreview_Light( } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ParticipateBottomBlockPreview_Dark( - @PreviewParameter(ParticipateBottomBlockDataProvider::class) state: ParticipateBottomBlockData, -) { - TangemTheme(isDark = true) { - Column(Modifier.background(TangemTheme.colors.background.secondary)) { - ParticipateBottomBlock( - purchasedWalletCount = state.purchasedWalletCount, - code = state.code, - shareLink = state.shareLink, - expectedAwards = state.expectedAwards, - onAgreementClick = state.onAgreementClick, - onShowCopySnackbar = state.onShowCopySnackbar, - onCopyClick = state.onCopyClick, - onShareClick = state.onShareClick, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun LessMoreButton_Light() { - TangemTheme(isDark = false) { - LessMoreButton( - isExpanded = remember { - mutableStateOf(false) - }, - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun LessMoreButton_Dark() { - TangemTheme(isDark = true) { +private fun LessMoreButtonPreview() { + TangemThemePreview { LessMoreButton( isExpanded = remember { mutableStateOf(false) @@ -420,7 +395,6 @@ private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterPro purchasedWalletCount = 0, expectedAwards = null, ), - ), ) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 74f9653422..39b216fb16 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -1,5 +1,7 @@ package com.tangem.feature.referral.ui +import android.content.res.Configuration +import android.content.res.Resources import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -10,9 +12,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -21,13 +21,15 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar +import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.models.DemoModeException @@ -48,6 +50,8 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier var isBottomSheetVisible by remember { mutableStateOf(value = false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val snackbarHostState = remember(::SnackbarHostState) + Scaffold( modifier = modifier, topBar = { @@ -56,10 +60,26 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier onBackClick = stateHolder.headerState.onBackClicked, ) }, + snackbarHost = { + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(bottom = TangemTheme.dimens.spacing108), + ) { + // TODO: use StateEvent + if (stateHolder.errorSnackbar != null) { + TangemSnackbar(data = it, actionOnNewLine = true) + } else { + CopiedTextSnackbar(it) + } + } + }, containerColor = TangemTheme.colors.background.secondary, ) { ReferralContent( stateHolder = stateHolder, + snackbarHostState = snackbarHostState, onAgreementClick = { stateHolder.analytics.onAgreementClicked.invoke() isBottomSheetVisible = true @@ -68,6 +88,26 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier ) } + val errorSnackbar = stateHolder.errorSnackbar + val coroutineScope = rememberCoroutineScope() + val resources = LocalContext.current.resources + + SideEffect { + if (errorSnackbar != null) { + coroutineScope.launch { + val result = snackbarHostState.showSnackbar( + message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable), + actionLabel = resources.getString(R.string.warning_button_ok), + duration = SnackbarDuration.Indefinite, + ) + + if (result == SnackbarResult.ActionPerformed) { + errorSnackbar.onOkClicked() + } + } + } + } + ReferralBottomSheet( sheetState = sheetState, isVisible = isBottomSheetVisible, @@ -79,11 +119,10 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier @Composable private fun ReferralContent( stateHolder: ReferralStateHolder, + snackbarHostState: SnackbarHostState, onAgreementClick: () -> Unit, modifier: Modifier = Modifier, ) { - val isCopyButtonPressed = remember { mutableStateOf(value = false) } - Box(modifier = modifier) { LazyColumn( modifier = Modifier.fillMaxSize(), @@ -93,14 +132,11 @@ private fun ReferralContent( item { ReferralInfo( stateHolder = stateHolder, + snackbarHostState = snackbarHostState, onAgreementClick = onAgreementClick, - onShowCopySnackbar = { isCopyButtonPressed.value = true }, ) } } - - ErrorSnackbarHost(errorSnackbar = stateHolder.errorSnackbar) - CopySnackbarHost(isCopyButtonPressed = isCopyButtonPressed) } } @@ -131,8 +167,8 @@ private fun Header() { @Composable private fun ReferralInfo( stateHolder: ReferralStateHolder, + snackbarHostState: SnackbarHostState, onAgreementClick: () -> Unit, - onShowCopySnackbar: () -> Unit, ) { when (val state = stateHolder.referralInfoState) { is ReferralInfoState.ParticipantContent -> { @@ -142,8 +178,8 @@ private fun ReferralInfo( code = state.code, shareLink = state.shareLink, expectedAwards = state.expectedAwards, + snackbarHostState = snackbarHostState, onAgreementClick = onAgreementClick, - onShowCopySnackbar = onShowCopySnackbar, onCopyClick = stateHolder.analytics.onCopyClicked, onShareClick = stateHolder.analytics.onShareClicked, ) @@ -333,122 +369,19 @@ private fun ShimmerInfo() { } } -// TODO() Replace component with component from ds -@Composable -private fun BoxScope.ErrorSnackbarHost(errorSnackbar: ErrorSnackbar?) { - if (errorSnackbar != null) { - val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } - val coroutineScope = rememberCoroutineScope() - - SnackbarHost( - hostState = snackbarHostState, - modifier = Modifier.align(Alignment.BottomCenter), - snackbar = { - Snackbar( - snackbarData = it, - modifier = Modifier.fillMaxWidth(), - actionOnNewLine = true, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius8), - containerColor = TangemTheme.colors.button.primary, - contentColor = TangemTheme.colors.text.primary2, - actionColor = TangemTheme.colors.text.primary2, - ) - }, - ) - - val actionLabel = stringResource(id = R.string.warning_button_ok) - val message = getMessageForErrorSnackbar(errorSnackbar) - SideEffect { - coroutineScope.launch { - val result = snackbarHostState.showSnackbar( - message = message, - actionLabel = actionLabel, - duration = SnackbarDuration.Indefinite, - ) - if (result == SnackbarResult.ActionPerformed) { - errorSnackbar.onOkClicked() - } - } - } - } -} - -// TODO() Replace component with component from ds -@Composable -private fun BoxScope.CopySnackbarHost(isCopyButtonPressed: MutableState) { - if (isCopyButtonPressed.value) { - val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) } - val coroutineScope = rememberCoroutineScope() - - var snackbarSize by remember { mutableIntStateOf(value = 0) } - val width = LocalConfiguration.current.screenWidthDp.dp - val snackbarWidth = with(LocalDensity.current) { snackbarSize.toDp() } - - SnackbarHost( - hostState = snackbarHostState, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(start = (width - snackbarWidth).div(2), bottom = TangemTheme.dimens.spacing12) - .fillMaxWidth(), - snackbar = { - Box( - modifier = Modifier - .onSizeChanged { snackbarSize = it.width } - .background( - color = TangemTheme.colors.icon.primary1, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius8), - ) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing14, - ), - ) { - Text( - text = it.visuals.message, - color = TangemTheme.colors.text.primary2, - style = TangemTheme.typography.body2, - ) - } - }, - ) - - val message = stringResource(id = R.string.referral_promo_code_copied) - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = message, - duration = SnackbarDuration.Short, - ) - isCopyButtonPressed.value = false - } - } - } -} - -@Composable -private fun getMessageForErrorSnackbar(errorSnackbar: ErrorSnackbar): String { - return when (errorSnackbar.throwable) { - is DemoModeException -> { - stringResource(id = R.string.alert_demo_feature_disabled) - } - - else -> { - if (errorSnackbar.throwable.cause != null) { - String.format( - format = stringResource(id = R.string.referral_error_failed_to_load_info_with_reason), - errorSnackbar.throwable.cause, - ) - } else { - stringResource(id = R.string.referral_error_failed_to_load_info) - } - } +private fun Resources.getMessageForErrorSnackbar(throwable: Throwable): String { + return when { + throwable is DemoModeException -> getString(R.string.alert_demo_feature_disabled) + throwable.cause != null -> getString(R.string.referral_error_failed_to_load_info_with_reason, throwable.cause) + else -> getString(R.string.referral_error_failed_to_load_info) } } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_Participant_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_ReferralScreen_Participant() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), @@ -475,38 +408,10 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_Participant_InDarkTheme() { - TangemTheme(isDark = true) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.ParticipantContent( - award = "10 USDT", - networkName = "Tron", - address = "ma80...zk8q2", - discount = "10%", - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - url = "", - expectedAwards = null, - ), - errorSnackbar = ErrorSnackbar(DemoModeException()) {}, - analytics = Analytics( - onAgreementClicked = {}, - onCopyClicked = {}, - onShareClicked = {}, - ), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_ReferralScreen_Participant_With_Referrals() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), @@ -549,9 +454,10 @@ private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_NonParticipant_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_ReferralScreen_NonParticipant() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), @@ -574,53 +480,10 @@ private fun Preview_ReferralScreen_NonParticipant_InLightTheme() { } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { - TangemTheme(isDark = true) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.NonParticipantContent( - award = "10 USDT", - networkName = "Tron", - discount = "10%", - url = "", - onParticipateClicked = {}, - ), - errorSnackbar = null, - analytics = Analytics( - onAgreementClicked = {}, - onCopyClicked = {}, - onShareClicked = {}, - ), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_ReferralScreen_Loading_InLightTheme() { - TangemTheme(isDark = false) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.Loading, - errorSnackbar = null, - analytics = Analytics( - onAgreementClicked = {}, - onCopyClicked = {}, - onShareClicked = {}, - ), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_ReferralScreen_Loading_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_ReferralScreen_Loading() { + TangemThemePreview { ReferralScreen( stateHolder = ReferralStateHolder( headerState = HeaderState(onBackClicked = {}), diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 4d0d072624..66e103efad 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index da17befee3..1c7d7f9cee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -6,10 +6,10 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter @@ -26,7 +26,7 @@ import javax.inject.Inject internal class SendFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var router: SendRouter diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index fcd33d986f..f2bf0e4d67 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -162,4 +162,28 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) } + + sealed interface Cardano { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + ) + + data object InsufficientBalanceToTransferCoin : Error( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + ) + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Error( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + ) + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index f2ea95f678..3d038ef6a6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -1,18 +1,11 @@ package com.tangem.features.send.impl.presentation.state -import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter @@ -20,18 +13,14 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter -import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientHistoryListConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter -import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientWalletListConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber import java.math.BigDecimal -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList") internal class SendStateFactory( private val clickIntents: SendClickIntents, private val stateRouterProvider: Provider, @@ -41,7 +30,6 @@ internal class SendStateFactory( private val cryptoCurrencyStatusProvider: Provider, private val feeCryptoCurrencyStatusProvider: Provider, private val isTapHelpPreviewEnabledProvider: Provider, - private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -79,14 +67,6 @@ internal class SendStateFactory( isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider, ) } - private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendRecipientWalletListConverter() - } - private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendRecipientHistoryListConverter( - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - ) - } private val sendSyncEditConverter by lazy(LazyThreadSafetyMode.NONE) { SendSyncEditConverter(currentStateProvider = currentStateProvider) } @@ -132,156 +112,6 @@ internal class SendStateFactory( } //endregion - //region recipient - fun onLoadedWalletsList(wallets: List): SendUiState { - val state = currentStateProvider() - return state.copy( - recipientState = state.recipientState?.copy( - wallets = recipientWalletListStateConverter.convert(wallets), - ), - ) - } - - fun onLoadedHistoryList(txHistory: List): SendUiState { - val state = currentStateProvider() - return state.copy( - recipientState = state.recipientState?.copy( - recent = recipientHistoryListStateConverter.convert(txHistory), - ), - ) - } - - fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - addressTextField = recipientState.addressTextField.copy(value = value), - memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress), - ), - ) - } - - fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - - val isValidMemo = validateWalletMemoUseCase( - memo = recipientState.memoTextField?.value.orEmpty(), - network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true - - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, - isValidating = false, - addressTextField = recipientState.addressTextField.copy( - error = when { - !isValidAddress -> resourceReference(R.string.send_recipient_address_error) - isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet) - else -> null - }, - isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet, - ), - ), - ) - } - - fun getOnRecipientAddressValidationStarted(): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy(isValidating = true), - ) - } - - fun getOnRecipientMemoValueChange(value: String): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - memoTextField = recipientState.memoTextField?.copy(value = value), - ), - ) - } - - fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - - val isValidMemo = validateWalletMemoUseCase( - memo = value, - network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } - val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses - ?.any { it.value == value } ?: true - - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, - isValidating = false, - memoTextField = recipientState.memoTextField?.copy( - isError = value.isNotEmpty() && !isValidMemo, - isEnabled = true, - ), - ), - ) - } - - fun getOnXAddressMemoState(): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - memoTextField = recipientState.memoTextField?.copy( - value = "", - isEnabled = false, - ), - ), - ) - } - - fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState { - val state = currentStateProvider() - val isEditState = stateRouterProvider().isEditState - val recipientState = state.getRecipientState(isEditState) ?: return state - val isNotValid = isAddressInWallet || !isValidAddress - return state.copyWrapped( - isEditState = isEditState, - recipientState = recipientState.copy( - recent = recipientState.recent.map { recent -> - recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY)) - }.toPersistentList(), - wallets = recipientState.wallets.map { wallet -> - wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY)) - }.toPersistentList(), - ), - ) - } - //endregion - //region send fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index a79ede8df6..81edeef29c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -1,20 +1,24 @@ package com.tangem.features.send.impl.presentation.state.confirm import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents @@ -23,6 +27,7 @@ import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.utils.getFiatString import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList @@ -46,6 +51,7 @@ internal class SendNotificationFactory( private val clickIntents: SendClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val validateTransactionUseCase: ValidateTransactionUseCase, ) { fun create(): Flow> = stateRouterProvider().currentState @@ -80,8 +86,9 @@ internal class SendNotificationFactory( addFeeUnreachableNotification(feeState.feeSelectorState) addExceedBalanceNotification(feeValue, sendingAmount) addExceedsBalanceNotification(feeState.fee) - addDustWarningNotification(feeValue, sendingAmount) + addDustWarningNotificationForSpecificBlockchains(feeValue, sendingAmount) addTransactionLimitErrorNotification(feeValue, sendingAmount) + // warnings addExistentialWarningNotification(feeValue, amountValue) addFeeCoverageNotification( @@ -92,6 +99,9 @@ internal class SendNotificationFactory( addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce) addTooHighNotification(feeState.feeSelectorState) addTooLowNotification(feeState) + + // blockchain specific + addCardanoNotifications(sendingAmount, feeState.fee, state) }.toImmutableList() } @@ -280,23 +290,35 @@ internal class SendNotificationFactory( } } - private suspend fun MutableList.addDustWarningNotification( - feeAmount: BigDecimal, - receivedAmount: BigDecimal, + private suspend fun MutableList.addDustWarningNotificationForSpecificBlockchains( + feeValue: BigDecimal, + sendingAmount: BigDecimal, ) { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val dustValue = currencyChecksRepository.getDustValue( - userWalletProvider().walletId, - cryptoCurrencyStatus.currency.network, - ) ?: return + val isCardano = BlockchainUtils.isCardano(cryptoCurrencyStatusProvider().currency.network.id.value) - if (checkDustLimits(feeAmount, receivedAmount, dustValue)) { - add( - SendNotification.Error.MinimumAmountError(dustValue.toPlainString()), - ) + if (!isCardano) { + addDustWarningNotification(feeValue, sendingAmount) } } + private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val change = when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + balance - (feeAmount + receivedAmount) + } + is CryptoCurrency.Token -> { + val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + balance - feeAmount + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + return receivedAmount < dustValue || isChangeLowerThanDust + } + private fun MutableList.addTooLowNotification(feeState: SendStates.FeeState) { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return @@ -398,13 +420,90 @@ internal class SendNotificationFactory( return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum } - private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + private suspend fun MutableList.addCardanoNotifications( + sendingAmount: BigDecimal, + fee: Fee?, + state: SendUiState, + ) { + val sendingCurrency = cryptoCurrencyStatusProvider().currency + if (!BlockchainUtils.isCardano(sendingCurrency.network.id.value)) return - val totalAmount = feeAmount + receivedAmount - val change = balance - totalAmount - val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO - return receivedAmount < dustValue || isChangeLowerThanDust + validateTransactionUseCase( + amount = sendingAmount.convertToAmount(sendingCurrency), + fee = fee ?: return, + memo = state.recipientState?.memoTextField?.value, + destination = requireNotNull(state.recipientState?.addressTextField?.value), + userWalletId = userWalletProvider().walletId, + network = sendingCurrency.network, + ).fold( + ifLeft = { + addCardanoTransactionValidationError( + error = it as? BlockchainSdkError.Cardano ?: return@fold, + sendingCurrency = sendingCurrency, + ) + }, + ifRight = { + (fee as? Fee.CardanoToken)?.let { + add( + SendNotification.Cardano.MinAdaValueCharged( + tokenName = sendingCurrency.name, + minAdaValue = it.minAdaValue.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + }, + ) + } + + private suspend fun MutableList.addCardanoTransactionValidationError( + error: BlockchainSdkError.Cardano, + sendingCurrency: CryptoCurrency, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + add(SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (sendingCurrency) { + is CryptoCurrency.Coin -> SendNotification.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + SendNotification.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name) + } + }.let(::add) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + val dustValue = currencyChecksRepository.getDustValue( + userWalletId = userWalletProvider().walletId, + network = sendingCurrency.network, + ) ?: return + + add( + SendNotification.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + } + } + + private suspend fun MutableList.addDustWarningNotification( + feeValue: BigDecimal, + sendingAmount: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val dustValue = currencyChecksRepository.getDustValue( + userWalletProvider().walletId, + cryptoCurrencyStatus.currency.network, + ) ?: return + + if (checkDustLimits(feeValue, sendingAmount, dustValue)) { + add( + SendNotification.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + ), + ) + } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 6cd4f8c31d..4f5ce27038 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -42,6 +42,7 @@ internal sealed class SendTextField { val label: TextReference, val isError: Boolean = false, val error: TextReference? = null, + val isValuePasted: Boolean, ) : SendTextField() data class RecipientMemo( @@ -54,6 +55,7 @@ internal sealed class SendTextField { val error: TextReference? = null, val disabledText: TextReference, val isEnabled: Boolean, + val isValuePasted: Boolean, ) : SendTextField() data class CustomFee( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt index b4ca6d643a..be5d2c2d54 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/RecipientStatePreviewData.kt @@ -28,6 +28,7 @@ internal object RecipientStatePreviewData { label = stringReference("Recipient"), isError = false, error = null, + isValuePasted = false, ), memoTextField = SendTextField.RecipientMemo( value = "", diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index 8c08c468e2..2eeda94fd5 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -36,7 +36,7 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {} - override fun onRecipientMemoValueChange(value: String) {} + override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {} override fun feeReload() {} @@ -65,7 +65,8 @@ internal object SendClickIntentsStub : SendClickIntents { reduceAmountByDiff: BigDecimal?, reduceAmountTo: BigDecimal?, clazz: Class, - ) {} + ) { + } override fun onNotificationCancel(clazz: Class) {} } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt new file mode 100644 index 0000000000..5fcb985783 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt @@ -0,0 +1,182 @@ +package com.tangem.features.send.impl.presentation.state.recipient + +import arrow.core.getOrElse +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.utils.Provider +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber + +internal class RecipientSendFactory( + private val stateRouterProvider: Provider, + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, +) { + private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) { + SendRecipientWalletListConverter() + } + private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) { + SendRecipientHistoryListConverter( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + + fun onLoadedWalletsList(wallets: List): SendUiState { + val state = currentStateProvider() + return state.copy( + recipientState = state.recipientState?.copy( + wallets = recipientWalletListStateConverter.convert(wallets), + ), + ) + } + + fun onLoadedHistoryList(txHistory: List): SendUiState { + val state = currentStateProvider() + return state.copy( + recipientState = state.recipientState?.copy( + recent = recipientHistoryListStateConverter.convert(txHistory), + ), + ) + } + + fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false, isValuePasted: Boolean): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + addressTextField = recipientState.addressTextField.copy(value = value, isValuePasted = isValuePasted), + memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress), + ), + ) + } + + fun getOnRecipientAddressValidState(value: String, isValidAddress: Boolean): SendUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + + val isValidMemo = validateWalletMemoUseCase( + memo = recipientState.memoTextField?.value.orEmpty(), + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e("Failed to validateWalletMemoUseCase: $it") + false + } + val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses + ?.any { it.value == value } ?: true + + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, + isValidating = false, + addressTextField = recipientState.addressTextField.copy( + error = when { + !isValidAddress -> resourceReference(R.string.send_recipient_address_error) + isAddressInWallet -> resourceReference(R.string.send_error_address_same_as_wallet) + else -> null + }, + isError = value.isNotEmpty() && !isValidAddress || isAddressInWallet, + ), + ), + ) + } + + fun getOnRecipientAddressValidationStarted(): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy(isValidating = true), + ) + } + + fun getOnRecipientMemoValueChange(value: String, isValuePasted: Boolean): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + memoTextField = recipientState.memoTextField?.copy( + value = value, + isValuePasted = isValuePasted, + ), + ), + ) + } + + fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + + val isValidMemo = validateWalletMemoUseCase( + memo = value, + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e("Failed to validateWalletMemoUseCase: $it") + false + } + val isAddressInWallet = cryptoCurrencyStatus.value.networkAddress?.availableAddresses + ?.any { it.value == value } ?: true + + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + isPrimaryButtonEnabled = isValidMemo && isValidAddress && !isAddressInWallet, + isValidating = false, + memoTextField = recipientState.memoTextField?.copy( + isError = value.isNotEmpty() && !isValidMemo, + isEnabled = true, + ), + ), + ) + } + + fun getOnXAddressMemoState(): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + memoTextField = recipientState.memoTextField?.copy( + value = "", + isEnabled = false, + ), + ), + ) + } + + fun getHiddenRecentListState(isAddressInWallet: Boolean, isValidAddress: Boolean): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val recipientState = state.getRecipientState(isEditState) ?: return state + val isNotValid = isAddressInWallet || !isValidAddress + return state.copyWrapped( + isEditState = isEditState, + recipientState = recipientState.copy( + recent = recipientState.recent.map { recent -> + recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY)) + }.toPersistentList(), + wallets = recipientState.wallets.map { wallet -> + wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY)) + }.toPersistentList(), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt index ab822de809..2365bc31e2 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientAddressFieldConverter.kt @@ -24,6 +24,7 @@ internal class SendRecipientAddressFieldConverter( error = resourceReference(R.string.send_recipient_address_error), placeholder = resourceReference(R.string.send_enter_address_field), label = resourceReference(R.string.send_recipient), + isValuePasted = false, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt index a539d72859..1525d0d6ab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -58,6 +58,7 @@ internal class SendRecipientMemoFieldConverter( error = resourceReference(R.string.send_memo_destination_tag_error), disabledText = resourceReference(R.string.send_additional_field_already_included), isEnabled = true, + isValuePasted = false, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 3f5d13ae28..672f385fc0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -34,10 +34,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType -import com.tangem.features.send.impl.presentation.utils.getFiatFormatted import com.tangem.features.send.impl.presentation.utils.getFiatString @Composable @@ -179,10 +179,10 @@ private fun SendingText( } if (feeFiat != null && sendingFiat != null) { - val sendingValue = getFiatFormatted( - value = sendingFiat, - currencySymbol = feeState.appCurrency.symbol, - currencyCode = feeState.appCurrency.code, + val sendingValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = sendingFiat, + fiatCurrencySymbol = feeState.appCurrency.symbol, + fiatCurrencyCode = feeState.appCurrency.code, ) val feeValue = getFiatString( value = feeState.fee?.amount?.value, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt index 4452c08cb6..dbd6b2043b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.amount +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -8,6 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData @@ -43,11 +45,12 @@ internal fun SendAmountContent( // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AmountFieldPreview_Light( - @PreviewParameter(AmountStatePreviewProvider::class) amountState: SendStates.AmountState, +private fun AmountFieldPreview( + @PreviewParameter(AmountFieldPreviewProvider::class) amountState: SendStates.AmountState, ) { - TangemTheme { + TangemThemePreview { SendAmountContent( amountState = amountState, isBalanceHiding = false, @@ -56,21 +59,7 @@ private fun AmountFieldPreview_Light( } } -@Preview -@Composable -private fun AmountFieldPreview_Dark( - @PreviewParameter(AmountStatePreviewProvider::class) amountState: SendStates.AmountState, -) { - TangemTheme(isDark = true) { - SendAmountContent( - amountState = amountState, - isBalanceHiding = false, - clickIntents = SendClickIntentsStub, - ) - } -} - -private class AmountStatePreviewProvider : PreviewParameterProvider { +private class AmountFieldPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( AmountStatePreviewData.amountState, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index 270d4954f9..f403cf423b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.fee +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeType @@ -107,21 +109,12 @@ private fun FooterText(onReadMoreClick: () -> Unit) { // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SendSpeedSelectorPreview_Light( +private fun SendSpeedSelectorPreview( @PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState, ) { - TangemTheme { - SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub) - } -} - -@Preview -@Composable -private fun SendSpeedSelectorPreview_Dark( - @PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt index 92c49f2dff..ec3e048834 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/ListItemWithIcon.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.recipient +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn @@ -27,6 +28,7 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R @@ -183,28 +185,12 @@ private fun ListItemLoading(modifier: Modifier = Modifier) { // region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ListItemWithIconPreview_Light( +private fun ListItemWithIconPreview( @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, ) { - TangemTheme { - ListItemWithIcon( - title = config.title, - subtitle = config.subtitle, - subtitleEndOffset = config.subtitleEndOffset, - subtitleIconRes = config.iconRes, - onClick = {}, - isLoading = config.isLoading, - ) - } -} - -@Preview -@Composable -private fun ListItemWithIconPreview_Dark( - @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ListItemWithIcon( title = config.title, subtitle = config.subtitle, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index 7885c8b3ad..38f13f0ab9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -70,7 +70,7 @@ internal fun SendRecipientContent( ) memoField( memoField = memoField, - onMemoChange = clickIntents::onRecipientMemoValueChange, + onMemoChange = { clickIntents.onRecipientMemoValueChange(it, true) }, ) listHeaderItem( titleRes = R.string.send_recipient_wallets_title, @@ -117,6 +117,7 @@ private fun LazyListScope.addressItem( isError = isError, isLoading = isValidating, error = address.error, + isValuePasted = address.isValuePasted, modifier = Modifier .background( color = TangemTheme.colors.background.action, @@ -143,6 +144,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM isError = memoField.isError, error = memoField.error, isReadOnly = !memoField.isEnabled, + isValuePasted = memoField.isValuePasted, ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index eeed6b1e9b..e11b62e872 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -32,6 +32,7 @@ internal fun TextFieldWithPaste( error: TextReference? = null, isError: Boolean = false, isReadOnly: Boolean = false, + isValuePasted: Boolean = false, ) { val (title, color) = when { isError && error != null -> error to TangemTheme.colors.text.warning @@ -65,6 +66,7 @@ internal fun TextFieldWithPaste( placeholderColor = placeholderColor, onValueChange = onValueChange, readOnly = isReadOnly, + isValuePasted = isValuePasted, modifier = Modifier .fillMaxWidth() .padding(top = TangemTheme.dimens.spacing8), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt index b3b5d066d2..504ab85abb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.send +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -16,6 +17,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendStates @@ -87,11 +89,10 @@ internal fun AmountBlock( // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AmountBlockPreview_Light( - @PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState, -) { - TangemTheme { +private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState) { + TangemThemePreview { AmountBlock( amountState = value, isClickDisabled = false, @@ -101,21 +102,6 @@ private fun AmountBlockPreview_Light( } } -@Preview -@Composable -private fun AmountBlockPreview_Dark( - @PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState, -) { - TangemTheme(isDark = true) { - AmountBlock( - amountState = value, - isClickDisabled = true, - isEditingDisabled = false, - onClick = {}, - ) - } -} - private class AmountBlockPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index d0fe044682..55d5985fa6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.send +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.R @@ -111,21 +113,10 @@ private fun BoxScope.FeeError(feeSelectorState: FeeSelectorState) { // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun FeeBlockPreview_Light(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { - TangemTheme { - FeeBlock( - feeState = value, - isClickDisabled = true, - onClick = {}, - ) - } -} - -@Preview -@Composable -private fun FeeBlockPreview_Dark(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { - TangemTheme(isDark = true) { +private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) { + TangemThemePreview { FeeBlock( feeState = value, isClickDisabled = true, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 3936e4985b..4839b83690 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.ui.send +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendTextField @@ -97,26 +99,12 @@ private fun MemoBlock(memo: SendTextField.RecipientMemo?) { // region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun RecipientBlockPreview_Light( +private fun RecipientBlockPreview( @PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState, ) { - TangemTheme { - RecipientBlock( - recipientState = value, - isClickDisabled = true, - isEditingDisabled = false, - onClick = {}, - ) - } -} - -@Preview -@Composable -private fun RecipientBlockPreview_Dark( - @PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { RecipientBlock( recipientState = value, isClickDisabled = true, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt index f2be7e6ed2..7c8a2becb6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt @@ -8,11 +8,8 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN import com.tangem.domain.appcurrency.model.AppCurrency import java.math.BigDecimal -import java.math.RoundingMode -private const val FIAT_DECIMALS = 2 private const val CRYPTO_FEE_DECIMALS = 6 -private const val FEE_MINIMUM_VALUE = 0.01 internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { if (amount == null) return null @@ -37,27 +34,9 @@ internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { if (value == null || rate == null) return EMPTY_BALANCE_SIGN val feeValue = value.multiply(rate) - return getFiatFormatted(feeValue, appCurrency.code, appCurrency.symbol) -} - -internal fun getFiatFormatted(value: BigDecimal?, currencyCode: String, currencySymbol: String): String { - val scaled = value?.setScale(FIAT_DECIMALS, RoundingMode.UP) ?: BigDecimal.ZERO - return if (scaled < BigDecimal(FEE_MINIMUM_VALUE)) { - buildString { - append(BigDecimalFormatter.CAN_BE_LOWER_SIGN) - append( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = BigDecimal(FEE_MINIMUM_VALUE), - fiatCurrencyCode = currencyCode, - fiatCurrencySymbol = currencySymbol, - ), - ) - } - } else { - BigDecimalFormatter.formatFiatAmount( - fiatAmount = value, - fiatCurrencyCode = currencyCode, - fiatCurrencySymbol = currencySymbol, - ) - } + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = feeValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 8eb2a331d5..457de01af1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -39,7 +39,7 @@ internal interface SendClickIntents { // region Recipient fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null) - fun onRecipientMemoValueChange(value: String) + fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false) // endregion // region Fee diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 1d5b01c162..32f84a0f7f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -30,10 +30,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.GetFeeUseCase -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet @@ -53,12 +50,10 @@ import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory import com.tangem.features.send.impl.presentation.state.fee.* +import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendFactory import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.DelayedWork -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* @@ -100,6 +95,7 @@ internal class SendViewModel @Inject constructor( private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, @DelayedWork private val coroutineScope: CoroutineScope, + validateTransactionUseCase: ValidateTransactionUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, validateWalletMemoUseCase: ValidateWalletMemoUseCase, @@ -133,10 +129,16 @@ internal class SendViewModel @Inject constructor( appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, - validateWalletMemoUseCase = validateWalletMemoUseCase, isTapHelpPreviewEnabledProvider = Provider { isTapHelpPreviewEnabled }, ) + private val recipientStateFactory = RecipientSendFactory( + stateRouterProvider = Provider { stateRouter }, + currentStateProvider = Provider { uiState }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + validateWalletMemoUseCase = validateWalletMemoUseCase, + ) + private val amountStateFactory = AmountStateFactory( stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState }, @@ -178,6 +180,7 @@ internal class SendViewModel @Inject constructor( clickIntents = this, analyticsEventHandler = analyticsEventHandler, getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, + validateTransactionUseCase = validateTransactionUseCase, ) private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) { @@ -407,14 +410,16 @@ internal class SendViewModel @Inject constructor( private fun getUserWallets() { viewModelScope.launch(dispatchers.main) { runCatching { - getWalletsUseCase.invokeSync() - ?.toAvailableWallets() - .orEmpty() + waitForDelay(delay = RECENT_LOAD_DELAY) { + getWalletsUseCase.invokeSync() + ?.toAvailableWallets() + .orEmpty() + } }.onSuccess { result -> userWallets = result - uiState = stateFactory.onLoadedWalletsList(wallets = userWallets) + uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets) }.onFailure { - uiState = stateFactory.onLoadedWalletsList(wallets = emptyList()) + uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList()) } } } @@ -450,11 +455,13 @@ internal class SendViewModel @Inject constructor( } private suspend fun getTxHistory() { - val txHistoryList = getFixedTxHistoryItemsUseCase.getSync( - userWalletId = userWalletId, - currency = cryptoCurrency, - ).getOrElse { emptyList() } - uiState = stateFactory.onLoadedHistoryList(txHistory = txHistoryList) + val txHistoryList = waitForDelay(delay = RECENT_LOAD_DELAY) { + getFixedTxHistoryItemsUseCase.getSync( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).getOrElse { emptyList() } + } + uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList) } private fun onStateActive() { @@ -588,7 +595,7 @@ internal class SendViewModel @Inject constructor( ) } - // endregion +// endregion // region amount state clicks override fun onCurrencyChangeClick(isFiat: Boolean) { @@ -614,23 +621,24 @@ internal class SendViewModel @Inject constructor( override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = stateFactory.onRecipientAddressValueChange(value) - uiState = stateFactory.getOnRecipientAddressValidationStarted() + uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null) + uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() val isValidAddress = validateAddress(value) - uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress) + uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress) type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) } autoNextFromRecipient(type, isValidAddress) } }.saveIn(addressValidationJobHolder) } - override fun onRecipientMemoValueChange(value: String) { + override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) { viewModelScope.launch { if (!checkIfXrpAddressValue(value)) { - uiState = stateFactory.getOnRecipientMemoValueChange(value) - uiState = stateFactory.getOnRecipientAddressValidationStarted() - val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty()) - uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress) + uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted) + uiState = recipientStateFactory.getOnRecipientAddressValidationStarted() + val recipientState = uiState.getRecipientState(stateRouter.isEditState) + val isValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty()) + uiState = recipientStateFactory.getOnRecipientMemoValidState(value, isValidAddress) } }.saveIn(memoValidationJobHolder) } @@ -649,16 +657,17 @@ internal class SendViewModel @Inject constructor( private suspend fun checkIfXrpAddressValue(value: String): Boolean { return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress -> - uiState = stateFactory.onRecipientAddressValueChange(value, isXAddress = true) - uiState = stateFactory.getOnXAddressMemoState() + uiState = + recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true) + uiState = recipientStateFactory.getOnXAddressMemoState() val isValidAddress = validateAddress(decodedAddress.address) - uiState = stateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) + uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress) true } ?: false } private fun onEnteredValidAddress(isValidAddress: Boolean, isAddressInWallet: Boolean) { - uiState = stateFactory.getHiddenRecentListState( + uiState = recipientStateFactory.getHiddenRecentListState( isAddressInWallet = isAddressInWallet, isValidAddress = isValidAddress, ) @@ -968,6 +977,7 @@ internal class SendViewModel @Inject constructor( private companion object { const val CHECK_FEE_UPDATE_DELAY = 60_000L const val BALANCE_UPDATE_DELAY = 11_000L + const val RECENT_LOAD_DELAY = 500L const val RU_LOCALE = "ru" const val EN_LOCALE = "en" diff --git a/features/staking/api/.gitignore b/features/staking/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/staking/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/staking/api/build.gradle.kts b/features/staking/api/build.gradle.kts new file mode 100644 index 0000000000..a14e2d42b0 --- /dev/null +++ b/features/staking/api/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.staking.api" +} + +dependencies { + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt new file mode 100644 index 0000000000..90ccdc617d --- /dev/null +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/featuretoggles/StakingFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.staking.api.featuretoggles + +/** + * Staking feature toggles + */ +interface StakingFeatureToggles { + + /** Availability of staking */ + val isStakingEnabled: Boolean +} \ No newline at end of file diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt new file mode 100644 index 0000000000..d2b216513e --- /dev/null +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.staking.api.navigation + +import androidx.fragment.app.Fragment + +interface StakingRouter { + + fun getEntryFragment(): Fragment +} \ No newline at end of file diff --git a/features/staking/impl/.gitignore b/features/staking/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/staking/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts new file mode 100644 index 0000000000..ac2ce74301 --- /dev/null +++ b/features/staking/impl/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.staking.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.appCompat) + implementation(deps.androidx.paging.runtime) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.material) + implementation(deps.arrow.core) + implementation(deps.lifecycle.compose) + implementation(deps.jodatime) + implementation(deps.timber) + + /** Compose */ + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.material3) + implementation(deps.compose.material) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + implementation(deps.compose.constraintLayout) + + /** Tangem SDKs */ + implementation(deps.tangem.card.core) + implementation(deps.tangem.blockchain) + + /** Core modules */ + implementation(projects.core.featuretoggles) + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.navigation) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + + /** Common */ + implementation(projects.common) + + /** Libs */ + implementation(projects.libs.crypto) + + /** Feature modules */ + implementation(projects.features.staking.api) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt new file mode 100644 index 0000000000..e40072f8bf --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingFeatureTogglesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.features.staking.impl.featuretoggles.DefaultStakingFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * DI module provides implementation of [StakingFeatureToggles] + */ +@Module +@InstallIn(SingletonComponent::class) +internal object StakingFeatureTogglesModule { + + @Provides + @Singleton + fun provideStakingFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { + return DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt new file mode 100644 index 0000000000..c351d87a6e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt @@ -0,0 +1,23 @@ +package com.tangem.features.staking.impl.di + +import com.tangem.features.staking.impl.navigation.DefaultStakingRouter +import com.tangem.features.staking.api.navigation.StakingRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [StakingRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +internal object StakingRouterModule { + + @Provides + @ActivityScoped + fun provideStakingRouter(): StakingRouter { + return DefaultStakingRouter() + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt new file mode 100644 index 0000000000..735c22b027 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/featuretoggles/DefaultStakingFeatureToggles.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles + +/** + * Default implementation of staking feature toggles + * + * @property featureTogglesManager manager for getting information about the availability of feature toggles + */ +internal class DefaultStakingFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : StakingFeatureToggles { + + override val isStakingEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_ENABLED") +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt new file mode 100644 index 0000000000..83cd830e4d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.staking.impl.navigation + +import androidx.fragment.app.Fragment + +internal class DefaultStakingRouter : InnerStakingRouter { + + override fun getEntryFragment(): Fragment = TODO() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt new file mode 100644 index 0000000000..7d7aa226d4 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -0,0 +1,5 @@ +package com.tangem.features.staking.impl.navigation + +import com.tangem.features.staking.api.navigation.StakingRouter + +interface InnerStakingRouter : StakingRouter \ No newline at end of file diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 208635ebbd..5998ddafd0 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { /** Domain */ implementation(projects.domain.tokens.models) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index c67891b454..68c61f2c71 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -8,6 +8,7 @@ import arrow.core.right import com.squareup.moshi.Moshi import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError @@ -20,11 +21,10 @@ import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.crypto.DataSignatureVerifier -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository @@ -48,7 +48,7 @@ internal class DefaultSwapRepository @Inject constructor( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val walletsStateHolder: WalletsStateHolder, + private val userWalletsListManager: UserWalletsListManager, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, moshi: Moshi, @@ -249,6 +249,7 @@ internal class DefaultSwapRepository @Inject constructor( fromContractAddress: String, fromNetwork: String, toContractAddress: String, + fromAddress: String, toNetwork: String, fromAmount: String, fromDecimals: Int, @@ -266,6 +267,7 @@ internal class DefaultSwapRepository @Inject constructor( fromContractAddress = fromContractAddress, fromNetwork = fromNetwork, toContractAddress = toContractAddress, + fromAddress = fromAddress, toNetwork = toNetwork, fromAmount = fromAmount, fromDecimals = fromDecimals, @@ -389,8 +391,8 @@ internal class DefaultSwapRepository @Inject constructor( blockchain = blockchain, extraDerivationPath = null, derivationStyleProvider = requireNotNull( - walletsStateHolder.userWalletsListManager - ?.selectedUserWalletSync + userWalletsListManager + .selectedUserWalletSync ?.scanResponse ?.derivationStyleProvider, ), diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt index 4ee7532cc7..ad6e5fb457 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt @@ -1,8 +1,8 @@ package com.tangem.feature.swap.converters import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.express.models.request.LeastTokenInfo -import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.converter.Converter diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 95b8469b2a..98881e9a09 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,7 +8,7 @@ import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.DefaultSwapRepository import com.tangem.feature.swap.DefaultSwapTransactionRepository import com.tangem.feature.swap.converters.ErrorsDataConverter @@ -33,7 +33,7 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - walletsStateHolder: WalletsStateHolder, + userWalletsListManager: UserWalletsListManager, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, ): SwapRepository { @@ -42,7 +42,7 @@ internal class SwapDataModule { tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - walletsStateHolder = walletsStateHolder, + userWalletsListManager = userWalletsListManager, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index 0a04d8950f..6ae1d7e0d1 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -58,6 +58,7 @@ interface SwapRepository { fromContractAddress: String, fromNetwork: String, toContractAddress: String, + fromAddress: String, toNetwork: String, fromAmount: String, fromDecimals: Int, diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index e0cb9a413a..73348d6d0b 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.demo) implementation(projects.domain.card) implementation(projects.domain.appCurrency.models) @@ -46,5 +47,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.timber) implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) implementation(deps.moshi) } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt index 318b5635de..a7e59700b2 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -9,4 +9,13 @@ sealed class Warning { data class MinAmountWarning(val dustValue: BigDecimal) : Warning() data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() + + sealed class Cardano : Warning() { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Cardano() + + data object InsufficientBalanceToTransferCoin : Cardano() + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Cardano() + } } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index 6ba68ddd31..bd29c42170 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -26,4 +26,6 @@ sealed class SwapTransactionState { data object UnknownError : SwapTransactionState() data class ExpressError(val dataError: DataError) : SwapTransactionState() + + data object DemoMode : SwapTransactionState() } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 0b77da443d..8bba53e589 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 @@ -5,12 +5,15 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchainsdk.utils.minimalAmount +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import com.tangem.domain.common.extensions.minimalAmount +import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -20,7 +23,10 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -34,12 +40,12 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.isNullOrZero import com.tangem.utils.toFiatString import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull @@ -59,6 +65,7 @@ internal class SwapInteractorImpl @Inject constructor( private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val walletManagersFacade: WalletManagersFacade, private val sendTransactionUseCase: SendTransactionUseCase, + private val createTransactionUseCase: CreateTransactionUseCase, private val quotesRepository: QuotesRepository, private val dispatcher: CoroutineDispatcherProvider, private val swapTransactionRepository: SwapTransactionRepository, @@ -66,6 +73,8 @@ internal class SwapInteractorImpl @Inject constructor( private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, + private val demoConfig: DemoConfig, + private val transactionRepository: TransactionRepository, ) : SwapInteractor { private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { @@ -343,6 +352,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = TxFeeState.Empty, + transactionFee = null, includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ) } @@ -372,6 +382,7 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, feeState: TxFeeState, + minAdaValue: BigDecimal?, ): List { val fromToken = fromTokenStatus.currency val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() @@ -379,6 +390,13 @@ internal class SwapInteractorImpl @Inject constructor( manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) manageReduceAmountWarning(warnings, fromTokenStatus, amount) + manageCardanoTransactionValidationWarnings( + warnings = warnings, + fromToken = fromToken, + amount = amount, + userWalletId = userWalletId, + minAdaValue = minAdaValue, + ) return warnings } @@ -407,23 +425,35 @@ internal class SwapInteractorImpl @Inject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, ) { + if (BlockchainUtils.isCardano(fromTokenStatus.currency.network.id.value)) return + val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue } - val dust = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) - val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO - if (dust != null && - !balance.isNullOrZero() && - amount.value < balance - ) { - val change = balance - (amount.value + fee) - val isChangeLowerThanDust = change < dust && change != BigDecimal.ZERO - val isShowWarning = amount.value + fee < dust || isChangeLowerThanDust - if (isShowWarning) { - warnings.add(Warning.MinAmountWarning(dust)) + + val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) ?: return + + val change = when (fromTokenStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO + balance - (fee + amount.value) } + is CryptoCurrency.Token -> { + val nativeTokenBalance = userWalletManager.getNativeTokenBalance( + fromTokenStatus.currency.network.id.value, + fromTokenStatus.currency.network.derivationPath.value, + ) + + nativeTokenBalance?.value?.minus(fee) ?: BigDecimal.ZERO + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + + if (amount.value < dustValue || isChangeLowerThanDust) { + warnings.add(Warning.MinAmountWarning(dustValue)) } } @@ -438,6 +468,72 @@ internal class SwapInteractorImpl @Inject constructor( } } + private suspend fun manageCardanoTransactionValidationWarnings( + warnings: MutableList, + fromToken: CryptoCurrency, + amount: SwapAmount, + userWalletId: UserWalletId, + minAdaValue: BigDecimal?, + ) { + transactionRepository.validateTransaction( + amount = amount.value.convertToAmount(fromToken), + fee = null, + memo = null, + destination = getTokenAddress(fromToken), + userWalletId = userWalletId, + network = fromToken.network, + ) + .fold( + onFailure = { + addCardanoTransactionValidationError( + warnings = warnings, + error = it as? BlockchainSdkError.Cardano ?: return@fold, + fromToken = fromToken, + userWalletId = userWalletId, + ) + }, + onSuccess = { + minAdaValue?.let { + warnings.add( + Warning.Cardano.MinAdaValueCharged( + tokenName = fromToken.name, + minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), + ), + ) + } + }, + ) + } + + private suspend fun addCardanoTransactionValidationError( + warnings: MutableList, + error: BlockchainSdkError.Cardano, + fromToken: CryptoCurrency, + userWalletId: UserWalletId, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (fromToken) { + is CryptoCurrency.Coin -> Warning.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) + } + } + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromToken.network) ?: return + + Warning.MinAmountWarning(dustValue) + } + } + .let(warnings::add) // add warning to the list + } + override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -473,6 +569,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet = currencyToGet.currency, amountToSwap = amountToSwap, fee = fee, + userWalletId = requireNotNull(getSelectedWallet()).walletId, ) } } @@ -523,29 +620,35 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet: CryptoCurrency, amountToSwap: String, fee: TxFee, + userWalletId: UserWalletId, ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, currencyToSend.decimals) val derivationPath = currencyToSend.network.derivationPath.value - val result = transactionManager.sendTransaction( - txData = SwapTxData( - networkId = networkId, - amountToSend = amountDecimal, - currencyToSend = swapCurrencyConverter.convert(currencyToSend), - feeAmount = fee.feeValue, - gasLimit = fee.gasLimit, - destinationAddress = swapData.transaction.txTo, - dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData, + val txData = createTransactionUseCase( + amount = amount.value.convertToAmount(currencyToSend), + fee = getFeeForTransaction( + fee = fee, + blockchain = Blockchain.fromId(currencyToSend.network.id.value), ), + memo = null, + destination = swapData.transaction.txTo, + userWalletId = userWalletId, + network = currencyToSend.network, + hash = (swapData.transaction as ExpressTransactionModel.DEX).txData, isSwap = true, - derivationPath = derivationPath, - analyticsData = AnalyticsData( - feeType = fee.feeType.getNameForAnalytics(), - tokenSymbol = currencyToSend.symbol, - ), + ).getOrElse { + Timber.e(it) + return SwapTransactionState.UnknownError + } + + val result = sendTransactionUseCase( + txData = txData, + userWallet = requireNotNull(getSelectedWallet()), + network = currencyToSend.network, ) - return when (result) { - is SendTxResult.Success -> { + return result.fold( + ifRight = { storeLastCryptoCurrencyId(currencyToGet) SwapTransactionState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( @@ -561,13 +664,18 @@ internal class SwapInteractorImpl @Inject constructor( txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) - } - SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTxResult.NetworkError -> SwapTransactionState.NetworkError - is SendTxResult.UnknownError -> SwapTransactionState.UnknownError - } + }, + ifLeft = { + when (it) { + SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode + else -> SwapTransactionState.UnknownError + } + }, + ) } @Suppress("LongMethod") @@ -583,13 +691,14 @@ internal class SwapInteractorImpl @Inject constructor( fromContractAddress = currencyToSend.currency.getContractAddress(), fromNetwork = currencyToSend.currency.network.backendId, toContractAddress = currencyToGet.currency.getContractAddress(), + fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), toNetwork = currencyToGet.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = currencyToGet.currency.decimals, providerId = swapProvider.providerId, rateType = RateType.FLOAT, - toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", + toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value.orEmpty(), refundAddress = currencyToSend.value.networkAddress?.defaultAddress?.value, refundExtraId = null, // currently always null ).getOrElse { return SwapTransactionState.ExpressError(it) } @@ -601,10 +710,11 @@ internal class SwapInteractorImpl @Inject constructor( currencyToSend.currency.network.backendId, exchangeDataCex.txExtraId, ) - if (txExtras == null && exchangeDataCex.txExtraId != null) { + val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError + if (txExtras == null && exchangeDataCex.txExtraId != null && !demoConfig.isDemoCardId(cardId)) { return SwapTransactionState.UnknownError } - val txData = walletManagersFacade.createTransaction( + val txData = createTransactionUseCase( amount = amount.value.convertToAmount(currencyToSend.currency), fee = getFeeForTransaction( fee = txFee, @@ -614,12 +724,13 @@ internal class SwapInteractorImpl @Inject constructor( destination = exchangeDataCex.txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, - )?.copy( - extras = txExtras, - ) + ).getOrElse { + Timber.e(it) + return SwapTransactionState.UnknownError + }.copy(extras = txExtras) val result = sendTransactionUseCase( - requireNotNull(txData), + txData = txData, userWallet = requireNotNull(getSelectedWallet()), network = currencyToSend.currency.network, ) @@ -634,6 +745,7 @@ internal class SwapInteractorImpl @Inject constructor( is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode else -> SwapTransactionState.UnknownError } }, @@ -818,8 +930,16 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { + val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> + getUnhandledFee( + amount = amount.value, + userWalletId = userWalletId, + cryptoCurrency = fromToken, + ) + } + val txFee = if (provider.type == ExchangeProviderType.CEX) { - getFeeForCex(amount, fromTokenStatus) + getFeeForCex(txFeeResult, fromTokenStatus) } else { TxFeeState.Empty } @@ -858,11 +978,13 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = isAllowedToSpend, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, txFee = txFee, + transactionFee = txFeeResult?.getOrNull(), includeFeeInAmount = includeFeeInAmount, ) } } + @Suppress("LongMethod") private suspend fun getQuotesState( exchangeProviderType: ExchangeProviderType, quoteDataModel: Either, @@ -873,6 +995,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, txFee: TxFeeState, + transactionFee: TransactionFee?, includeFeeInAmount: IncludeFeeInAmount, ): SwapState { return quoteDataModel.fold( @@ -886,7 +1009,12 @@ internal class SwapInteractorImpl @Inject constructor( swapData = null, txFeeState = txFee, ).copy( - warnings = manageWarnings(fromToken, amount, txFee), + warnings = manageWarnings( + fromTokenStatus = fromToken, + amount = amount, + feeState = txFee, + minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, + ), ) when (exchangeProviderType) { @@ -1048,13 +1176,14 @@ internal class SwapInteractorImpl @Inject constructor( fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), + fromAddress = fromToken.value.networkAddress?.defaultAddress?.value.orEmpty(), toNetwork = toToken.currency.network.backendId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.currency.decimals, providerId = provider.providerId, rateType = RateType.FLOAT, - toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "", + toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(), ).fold( ifRight = { swapData -> val feeData = transactionManager.getFee( @@ -1089,7 +1218,14 @@ internal class SwapInteractorImpl @Inject constructor( ) swapState.copy( permissionState = PermissionDataState.Empty, - warnings = manageWarnings(fromToken, amount, txFeeState), + warnings = manageWarnings( + fromToken, + amount, + txFeeState, + (feeData as? ProxyFees.SingleFee)?.let { + (it.singleFee as? ProxyFee.CardanoToken)?.minAdaValue + }, + ), preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = true, isBalanceEnough = isBalanceIncludeFeeEnough, @@ -1155,23 +1291,26 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun getFeeForCex(amount: SwapAmount, fromToken: CryptoCurrencyStatus): TxFeeState { - getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> - val txFeeResult = estimateFeeUseCase( - amount = amount.value, - userWalletId = userWalletId, - cryptoCurrency = fromToken.currency, - ).firstOrNull() - return txFeeResult?.fold( - ifLeft = { - TxFeeState.Empty - }, - ifRight = { txFee -> - txFee.toTxFeeState(fromToken.currency) - }, - ) ?: TxFeeState.Empty - } - return TxFeeState.Empty + private suspend fun getFeeForCex( + txFeeResult: Either?, + fromToken: CryptoCurrencyStatus, + ): TxFeeState { + return txFeeResult?.fold( + ifLeft = { TxFeeState.Empty }, + ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency) }, + ) ?: TxFeeState.Empty + } + + private suspend fun getUnhandledFee( + amount: BigDecimal, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Either? { + return estimateFeeUseCase( + amount = amount, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + ).firstOrNull() } @Suppress("LongParameterList", "LongMethod") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index afb2f253a4..1a6a6e8b22 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -11,9 +11,10 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* import com.tangem.feature.swap.domain.api.SwapRepository @@ -40,6 +41,7 @@ class SwapDomainModule { @SwapScope getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @SwapScope sendTransactionUseCase: SendTransactionUseCase, + @SwapScope createTransactionUseCase: CreateTransactionUseCase, quotesRepository: QuotesRepository, swapTransactionRepository: SwapTransactionRepository, appCurrencyRepository: AppCurrencyRepository, @@ -48,6 +50,7 @@ class SwapDomainModule { coroutineDispatcherProvider: CoroutineDispatcherProvider, initialToCurrencyResolver: InitialToCurrencyResolver, currenciesRepository: CurrenciesRepository, + transactionRepository: TransactionRepository, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -57,6 +60,7 @@ class SwapDomainModule { getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, sendTransactionUseCase = sendTransactionUseCase, + createTransactionUseCase = createTransactionUseCase, quotesRepository = quotesRepository, walletManagersFacade = walletManagersFacade, dispatcher = coroutineDispatcherProvider, @@ -65,6 +69,8 @@ class SwapDomainModule { currencyChecksRepository = currencyChecksRepository, currenciesRepository = currenciesRepository, initialToCurrencyResolver = initialToCurrencyResolver, + demoConfig = DemoConfig(), + transactionRepository = transactionRepository, ) } @@ -79,8 +85,8 @@ class SwapDomainModule { @SwapScope @Provides @Singleton - fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) + fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletSyncUseCase { + return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) } @Provides @@ -106,13 +112,11 @@ class SwapDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, - dispatchers: CoroutineDispatcherProvider, ): GetCardTokensListUseCase { return GetCardTokensListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, - dispatchers = dispatchers, ) } @@ -122,6 +126,15 @@ class SwapDomainModule { return IsDemoCardUseCase(config = DemoConfig()) } + @SwapScope + @Provides + @Singleton + fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase { + return CreateTransactionUseCase( + transactionRepository = transactionRepository, + ) + } + @SwapScope @Provides @Singleton diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index c78375b5e4..643cf8f201 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.settings) @@ -56,6 +57,9 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.tokendetails.api) + /** Libs */ + implementation(projects.libs.crypto) + /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.compose.accompanist.webView) @@ -67,5 +71,4 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) - } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index e5db542bba..cd22de2a3b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -20,7 +20,6 @@ data class SwapStateHolder( val alert: SwapWarning.GenericWarning? = null, val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, - val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01 val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, @@ -111,6 +110,7 @@ sealed interface SwapWarning { val type: GenericWarningType = GenericWarningType.OTHER, val onClick: () -> Unit, ) : SwapWarning + data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning @@ -118,6 +118,16 @@ sealed interface SwapWarning { data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning + + sealed interface Cardano : SwapWarning { + val notificationConfig: NotificationConfig + + data class MinAdaValueCharged(override val notificationConfig: NotificationConfig) : Cardano + + data class InsufficientBalanceToTransferCoin(override val notificationConfig: NotificationConfig) : Cardano + + data class InsufficientBalanceToTransferToken(override val notificationConfig: NotificationConfig) : Cardano + } } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index bd84a05cac..2a873d453c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -15,7 +15,6 @@ data class UiActions( val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, val onReduceAmount: (SwapAmount) -> Unit, - val onReduceAmountIgnoreClick: () -> Unit, val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 3b16f0df21..6409755135 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.swap.router.CustomTabsManager import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -25,7 +25,7 @@ import javax.inject.Inject class SwapFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var reduxNavController: ReduxNavController diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 35f1fb8fe4..8b1ac6f243 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.FeeItemState @@ -168,7 +169,7 @@ private fun ChooseFeeBottomSheetContent_Preview() { ), ).toImmutableList() Column { - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { ChooseFeeBottomSheetContent( ChooseFeeBottomSheetConfig( selectedFee = FeeType.NORMAL, @@ -183,7 +184,7 @@ private fun ChooseFeeBottomSheetContent_Preview() { SpacerH24() - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { ChooseFeeBottomSheetContent( ChooseFeeBottomSheetConfig( selectedFee = FeeType.NORMAL, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 4015137fcd..960b9d3373 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState @@ -128,7 +129,7 @@ private fun ChooseProviderBottomSheet_Preview() { alertText = stringReference("Unavailable"), ), ) - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { ChooseProviderBottomSheetContent( ChooseProviderBottomSheetConfig( selectedProviderId = "1", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt index f379ae3511..e4c3b974ca 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.components.rows.SimpleActionRow import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.FeeItemState @@ -63,13 +64,13 @@ private fun FeeItemPreview() { onClick = {}, ) Column { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { FeeItem(state = state) } SpacerH24() - TangemTheme(isDark = true) { + TangemThemePreview(isDark = true) { FeeItem(state = state) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 723d81e622..2707fa77b2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -27,6 +28,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState @@ -390,25 +392,12 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) { // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ProviderItemPreview_Light( +private fun ProviderItemPreview( @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, ) { - TangemTheme { - ProviderItem( - modifier = Modifier.background(TangemTheme.colors.background.action), - state = state.first, - isSelected = state.second, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun ProviderItemPreview_Dark( - @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ProviderItem( modifier = Modifier.background(TangemTheme.colors.background.action), state = state.first, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index e65136aa1a..17e7bfb369 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -88,7 +88,6 @@ internal class StateBuilder( onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), shouldShowMaxAmount = false, - reduceAmountIgnore = false, priceImpact = PriceImpact.Empty(), ) } @@ -217,7 +216,6 @@ internal class StateBuilder( val warnings = getWarningsForSuccessState( quoteModel = quoteModel, fromToken = fromToken, - ignoreAmountReduce = uiStateHolder.reduceAmountIgnore, selectedFeeType = selectedFeeType, ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) @@ -317,11 +315,10 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, - ignoreAmountReduce: Boolean, selectedFeeType: FeeType, ): List { val warnings = mutableListOf() - maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce) + maybeAddDomainWarnings(quoteModel, warnings) maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType) @@ -357,11 +354,7 @@ internal class StateBuilder( } } - private fun maybeAddDomainWarnings( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ignoreAmountReduce: Boolean, - ) { + private fun maybeAddDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { quoteModel.warnings.forEach { when (it) { is Warning.ExistentialDepositWarning -> { @@ -399,24 +392,30 @@ internal class StateBuilder( ) } is Warning.ReduceAmountWarning -> { - if (!ignoreAmountReduce) { - warnings.add( - SwapWarning.ReduceAmount( - notificationConfig = createReduceAmountNotificationConfig( - currencyName = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, - amount = it.tezosFeeThreshold.toPlainString(), - onConfirmClick = { - val fromAmount = quoteModel.fromTokenInfo.tokenAmount - val patchedAmount = fromAmount.copy( - value = fromAmount.value - it.tezosFeeThreshold, - ) - actions.onReduceAmount(patchedAmount) - }, - onDismissClick = actions.onReduceAmountIgnoreClick, - ), + warnings.add( + SwapWarning.ReduceAmount( + notificationConfig = createReduceAmountNotificationConfig( + currencyName = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, + amount = it.tezosFeeThreshold.toPlainString(), + onConfirmClick = { + val fromAmount = quoteModel.fromTokenInfo.tokenAmount + val patchedAmount = fromAmount.copy( + value = fromAmount.value - it.tezosFeeThreshold, + ) + actions.onReduceAmount(patchedAmount) + }, ), - ) - } + ), + ) + } + Warning.Cardano.InsufficientBalanceToTransferCoin -> { + warnings.add(createInsufficientBalanceToTransferCoin()) + } + is Warning.Cardano.InsufficientBalanceToTransferToken -> { + warnings.add(createInsufficientBalanceToTransferToken(tokenName = it.tokenName)) + } + is Warning.Cardano.MinAdaValueCharged -> { + warnings.add(createMinAdaValueCharged(minAdaValue = it.minAdaValue, tokenName = it.tokenName)) } } } @@ -1016,6 +1015,18 @@ internal class StateBuilder( ) } + fun createDemoModeAlert(uiState: SwapStateHolder, onAlertClick: () -> Unit): SwapStateHolder { + return uiState.copy( + alert = SwapWarning.GenericWarning( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + onClick = onAlertClick, + type = GenericWarningType.OTHER, + ), + changeCardsButtonState = ChangeCardsButtonState.ENABLED, + ) + } + private fun getProviderErrorMessage(dataError: DataError): TextReference? { return when (dataError) { is DataError.SwapsAreUnavailableNowError -> resourceReference( @@ -1379,17 +1390,14 @@ internal class StateBuilder( currencyName: String, amount: String, onConfirmClick: () -> Unit, - onDismissClick: () -> Unit, ): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.send_notification_high_fee_title), subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), iconResId = R.drawable.img_attention_20, - buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( - primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), - onPrimaryClick = onConfirmClick, - secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore), - onSecondaryClick = onDismissClick, + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), + onClick = onConfirmClick, ), ) } @@ -1433,6 +1441,42 @@ internal class StateBuilder( iconResId = R.drawable.img_attention_20, ) } + + private fun createMinAdaValueCharged(minAdaValue: String, tokenName: String): SwapWarning { + return SwapWarning.Cardano.MinAdaValueCharged( + NotificationConfig( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + iconResId = R.drawable.img_attention_20, + ), + ) + } + + private fun createInsufficientBalanceToTransferCoin(): SwapWarning { + return SwapWarning.Cardano.InsufficientBalanceToTransferCoin( + NotificationConfig( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + iconResId = R.drawable.img_attention_20, + ), + ) + } + + private fun createInsufficientBalanceToTransferToken(tokenName: String): SwapWarning { + return SwapWarning.Cardano.InsufficientBalanceToTransferToken( + NotificationConfig( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + iconResId = R.drawable.img_attention_20, + ), + ) + } // end region private fun getShortAddressValue(fullAddress: String): String { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt index b915b4c0a5..8ac175588a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.ApprovePermissionButton import com.tangem.feature.swap.models.ApproveType import com.tangem.feature.swap.models.CancelPermissionButton @@ -291,17 +293,10 @@ private fun getTitleForApproveType(approveType: ApproveType): String = when (app // region preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AgreementBottomSheet_InLightTheme() { - TangemTheme(isDark = false) { - SwapPermissionBottomSheetContent(content = previewData) - } -} - -@Preview -@Composable -private fun Preview_AgreementBottomSheet_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_AgreementBottomSheet() { + TangemThemePreview { SwapPermissionBottomSheetContent(content = previewData) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index e68875f3ee..0cdecf9771 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -30,6 +31,7 @@ import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* @@ -389,7 +391,8 @@ private fun SwapWarnings(warnings: List) { }, ) } - else -> {} + is SwapWarning.Cardano -> Notification(config = warning.notificationConfig) + SwapWarning.InsufficientFunds -> Unit } SpacerH8() } @@ -497,7 +500,6 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, - reduceAmountIgnore = false, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), @@ -513,9 +515,10 @@ private val state = SwapStateHolder( ) @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun SwapScreenContentPreview() { - TangemTheme(isDark = false) { + TangemThemePreview { SwapScreenContent(state = state, modifier = Modifier) } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 930039d697..081b492357 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -27,6 +27,7 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.ImmutableList @@ -303,7 +304,7 @@ private val title = TokenToSelectState.Title( @Preview @Composable private fun TokenScreenPreview() { - TangemTheme(isDark = false) { + TangemThemePreview { SwapSelectTokenScreen( state = SwapSelectTokenStateHolder( availableTokens = listOf(title, token, token, token).toImmutableList(), @@ -320,7 +321,7 @@ private fun TokenScreenPreview() { @Preview @Composable private fun EmptyTokensListPreview() { - TangemTheme(isDark = false) { + TangemThemePreview { EmptyTokensList() } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index a562a22435..11ade5db09 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -18,6 +19,7 @@ import com.tangem.core.ui.components.inputrow.InputRowImage import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.models.SwapSuccessStateHolder import com.tangem.feature.swap.presentation.R @@ -167,19 +169,11 @@ private val state = SwapSuccessStateHolder( ) @Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_Success_InLightTheme() { - TangemTheme(isDark = false) { +private fun Preview_Success() { + TangemThemePreview { SwapSuccessScreen(state) {} } } - -@Preview(showBackground = true) -@Composable -private fun Preview_Success_InDarkTheme() { - TangemTheme(isDark = true) { - SwapSuccessScreen(state) {} - } -} - // endregion preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index bd77a3f66e..db9d18b78e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -38,6 +38,7 @@ import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.TransactionCardType @@ -498,7 +499,7 @@ private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: In @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCard_InLightTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreview() } } @@ -506,7 +507,7 @@ private fun Preview_TransactionCard_InLightTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithPriceImpact_InLightTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithPriceImpact() } } @@ -514,7 +515,7 @@ private fun Preview_TransactionCardWithPriceImpact_InLightTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithoutPriceImpact_InLightTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithoutPriceImpact() } } @@ -522,7 +523,7 @@ private fun Preview_TransactionCardWithoutPriceImpact_InLightTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCard_InDarkTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreview() } } @@ -530,7 +531,7 @@ private fun Preview_TransactionCard_InDarkTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithPriceImpact_InDarkTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithPriceImpact() } } @@ -538,7 +539,7 @@ private fun Preview_TransactionCardWithPriceImpact_InDarkTheme() { @Preview(widthDp = 328, heightDp = 116, showBackground = true) @Composable private fun Preview_TransactionCardWithoutPriceImpact_InDarkTheme() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TransactionCardPreviewWithoutPriceImpact() } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt index eb2df9fe19..00a27d8a22 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -14,6 +15,7 @@ import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig @Composable @@ -55,21 +57,10 @@ private fun WebViewBottomSheetContent(content: WebViewBottomSheetConfig) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_WebViewBottomSheetContent_InLightTheme() { - TangemTheme(isDark = false) { - WebViewBottomSheetContent( - content = WebViewBottomSheetConfig( - url = "https://tangem.com/en/", - ), - ) - } -} - -@Preview -@Composable -private fun Preview_WebViewBottomSheetContent_InDarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_WebViewBottomSheetContent() { + TangemThemePreview { WebViewBottomSheetContent( content = WebViewBottomSheetConfig( url = "https://tangem.com/en/", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d9598de96f..2adc1f3505 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -540,6 +540,12 @@ internal class SwapViewModel @Inject constructor( is SwapTransactionState.UserCancelled -> { startLoadingQuotesFromLastState() } + is SwapTransactionState.DemoMode -> { + startLoadingQuotesFromLastState() + uiState = stateBuilder.createDemoModeAlert(uiState) { + uiState = stateBuilder.clearAlert(uiState) + } + } else -> { startLoadingQuotesFromLastState() uiState = stateBuilder.createErrorTransaction(uiState, it) { @@ -881,12 +887,6 @@ internal class SwapViewModel @Inject constructor( }, onMaxAmountSelected = ::onMaxAmountClicked, onReduceAmount = ::onReduceAmountClicked, - onReduceAmountIgnoreClick = { - uiState = uiState.copy( - reduceAmountIgnore = true, - warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount }, - ) - }, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt new file mode 100644 index 0000000000..a64be25542 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt @@ -0,0 +1,9 @@ +package com.tangem.features.tester.api + +/** + * Interface for app restarter + */ +interface AppRestarter { + + fun restart() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt new file mode 100644 index 0000000000..22d2d4d3a9 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.tester + +import android.app.Activity + +/** + * Wraps the main activity class to avoid type erasure issues during injection. + * + * @property clazz activity class + */ +class ActivityClassWrapper( + val clazz: Class, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt new file mode 100644 index 0000000000..34cd927e71 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.tester.apprestarter + +import android.app.Activity +import android.content.Context +import android.content.Intent +import com.tangem.feature.tester.ActivityClassWrapper +import com.tangem.features.tester.api.AppRestarter + +/** + * Entity that kills the process and restarts the main activity + * @property context Activity context + */ +internal class DefaultAppRestarter( + private val context: Context, + private val activityClassWrapper: ActivityClassWrapper, +) : AppRestarter { + + override fun restart() { + if (context !is Activity) return + + context.finish() + context.startActivity( + Intent(context, activityClassWrapper.clazz).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP }, + ) + Runtime.getRuntime().exit(0) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt new file mode 100644 index 0000000000..4891a60f12 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.tester.di + +import android.content.Context +import com.tangem.feature.tester.ActivityClassWrapper +import com.tangem.feature.tester.apprestarter.DefaultAppRestarter +import com.tangem.features.tester.api.AppRestarter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal object RestarterModule { + + @Provides + @ActivityScoped + fun provideAppRestarter( + @ActivityContext context: Context, + activityClassWrapper: ActivityClassWrapper, + ): AppRestarter { + return DefaultAppRestarter(context, activityClassWrapper) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 13c5799f38..ef7ab21998 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -6,10 +6,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen @@ -18,6 +18,7 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen +import com.tangem.features.tester.api.AppRestarter import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -27,12 +28,15 @@ import javax.inject.Inject internal class TesterActivity : ComposeActivity() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies /** Router for inner feature navigation */ @Inject lateinit var testerRouter: TesterRouter + @Inject + lateinit var appRestarter: AppRestarter + private val innerTesterRouter: InnerTesterRouter get() = requireNotNull(testerRouter as? InnerTesterRouter) { "TesterRouter must be InnerTesterRouter for tester feature" @@ -66,7 +70,7 @@ internal class TesterActivity : ComposeActivity() { composable(route = TesterScreen.FEATURE_TOGGLES.name) { val viewModel = hiltViewModel().apply { - setupNavigation(innerTesterRouter) + setupInteractions(innerTesterRouter, appRestarter) } FeatureTogglesScreen(state = viewModel.uiState) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt index fa65d935e4..c814bf750b 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt @@ -3,15 +3,16 @@ package com.tangem.feature.tester.presentation.actions import com.tangem.domain.apptheme.model.AppThemeMode internal data class TesterActionsContentState( - val onBackClick: () -> Unit, val hideAllCurrenciesConfig: HideAllCurrenciesConfig, val toggleAppThemeConfig: ToggleAppThemeConfig, + val onBackClick: () -> Unit, + val onApplyChangesClick: () -> Unit, ) internal sealed class HideAllCurrenciesConfig { data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesConfig() - object Progress : HideAllCurrenciesConfig() + data object Progress : HideAllCurrenciesConfig() } internal data class ToggleAppThemeConfig( diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index f8d9df0a78..1cc0845d8c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation.actions +import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -12,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.impl.R @@ -76,26 +78,20 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) { ) { TesterActionsScreen( state = TesterActionsContentState( - onBackClick = {}, hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable {}, toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT) {}, + onBackClick = {}, + onApplyChangesClick = {}, ), ) } } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TesterActionsScreenPreview_Light() { - TangemTheme { - TesterActionsScreenSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun TesterActionsScreenPreview_Dark() { - TangemTheme(isDark = true) { +private fun TesterActionsScreenPreview() { + TangemThemePreview { TesterActionsScreenSample() } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index b919f37132..514edb448d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -31,9 +31,10 @@ internal class TesterActionsViewModel @Inject constructor( private val initialState: TesterActionsContentState get() = TesterActionsContentState( - onBackClick = { /* no-op */ }, hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this::hideAllCurrencies), toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT, this::toggleAppTheme), + onBackClick = { /* no-op */ }, + onApplyChangesClick = { /* no-op */ }, ) init { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt index 4c55258e0d..736e01b6c6 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/state/FeatureTogglesContentState.kt @@ -8,9 +8,11 @@ import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatur * @property featureToggles feature toggles list * @property onBackClick the lambda to be invoked when back button is pressed * @property onToggleValueChange the lambda to be invoked when switch button is pressed + * @property onApplyChangesClick the lambda to be invoked when apply changes button is pressed */ internal data class FeatureTogglesContentState( val featureToggles: List, - val onBackClick: () -> Unit, val onToggleValueChange: (String, Boolean) -> Unit, + val onBackClick: () -> Unit, + val onApplyChangesClick: () -> Unit, ) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt index 7ce0c9ff6d..b43d13d2f3 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation.featuretoggles.ui +import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -18,8 +19,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState @@ -49,6 +52,14 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) { onCheckedChange = { isChange -> state.onToggleValueChange(featureToggle.name, isChange) }, ) } + item { + PrimaryButton( + text = stringResource(id = R.string.apply_changes), + onClick = state.onApplyChangesClick, + modifier = Modifier.fillMaxWidth() + .padding(TangemTheme.dimens.spacing16), + ) + } } } @@ -81,26 +92,10 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewFeatureTogglesScreen_InLightTheme() { - TangemTheme(isDark = false) { - FeatureTogglesScreen( - state = FeatureTogglesContentState( - featureToggles = listOf( - TesterFeatureToggle(name = "FEATURE_TOGGLE_1", isEnabled = true), - TesterFeatureToggle(name = "FEATURE_TOGGLE_2", isEnabled = false), - ), - onToggleValueChange = { _, _ -> }, - onBackClick = {}, - ), - ) - } -} - -@Preview -@Composable -private fun PreviewFeatureTogglesScreen_InDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewFeatureTogglesScreen() { + TangemThemePreview { FeatureTogglesScreen( state = FeatureTogglesContentState( featureToggles = listOf( @@ -109,6 +104,7 @@ private fun PreviewFeatureTogglesScreen_InDarkTheme() { ), onToggleValueChange = { _, _ -> }, onBackClick = {}, + onApplyChangesClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index 037946a145..d3459ac31f 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -10,6 +10,7 @@ import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.features.tester.api.AppRestarter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -38,16 +39,20 @@ internal class FeatureTogglesViewModel @Inject constructor( "Feature toggle manager must be mutable (debug build type)" } - /** Setup navigation state property by router [router] */ - fun setupNavigation(router: InnerTesterRouter) { - uiState = uiState.copy(onBackClick = router::back) + /** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */ + fun setupInteractions(router: InnerTesterRouter, appRestarter: AppRestarter) { + uiState = uiState.copy( + onBackClick = router::back, + onApplyChangesClick = appRestarter::restart, + ) } private fun initState(): FeatureTogglesContentState { return FeatureTogglesContentState( featureToggles = mutableFeatureTogglesManager.getTesterFeatureToggles(), - onBackClick = {}, onToggleValueChange = ::onToggleValueChange, + onBackClick = {}, + onApplyChangesClick = {}, ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index 4e5d7f056f..39d9c5b95c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation.menu.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -15,6 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState @@ -64,23 +66,10 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun PreviewTesterMenuScreen_InLightTheme() { - TangemTheme(isDark = false) { - TesterMenuScreen( - state = TesterMenuContentState( - onBackClick = {}, - onFeatureTogglesClick = {}, - onTesterActionsClick = {}, - ), - ) - } -} - -@Preview -@Composable -private fun PreviewTesterMenuScreen_InDarkTheme() { - TangemTheme(isDark = true) { +private fun PreviewTesterMenuScreen() { + TangemThemePreview { TesterMenuScreen( state = TesterMenuContentState( onBackClick = {}, diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 9a5a5846b6..ffc3114361 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ Tester menu Feature toggles + Apply changes Stand toggles Tester actions Hide all currencies diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 6db0120517..5c435284d7 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) @@ -73,6 +74,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.transaction) /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index bea2fb5a9f..94200fbd59 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -4,10 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel @@ -19,7 +19,7 @@ import javax.inject.Inject internal class TokenDetailsFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter 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..b16fe4ac2c 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.Receive(onClick = {}), - TokenDetailsActionButton.Swap(enabled = true, onClick = {}), + TokenDetailsActionButton.Buy(dimContent = false, onClick = {}), + TokenDetailsActionButton.Send(dimContent = false, onClick = {}), + TokenDetailsActionButton.Receive(onClick = {}, onLongClick = null), + TokenDetailsActionButton.Swap(dimContent = false, onClick = {}), ) val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index cf4d420f32..998dbf0aff 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -37,12 +37,12 @@ internal class TokenDetailsNotificationsAnalyticsSender( ) is TokenDetailsNotification.NetworksUnreachable, is TokenDetailsNotification.ExistentialDeposit, - is TokenDetailsNotification.HasPendingTransactions, is TokenDetailsNotification.NetworksNoAccount, is TokenDetailsNotification.TopUpWithoutReserve, is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.SwapPromo, is TokenDetailsNotification.NetworkShutdown, + is TokenDetailsNotification.HederaAssociateWarning, -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index 63130c7889..c5b6078527 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -11,46 +11,53 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { /** Lambda be invoked when manage button is clicked */ abstract val onClick: () -> Unit + /** Lambda be invoked when manage button is long clicked */ + open val onLongClick: (() -> TextReference?)? = null + /** * Buy * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Buy button is clicked */ - data class Buy(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Buy(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) /** * Send * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Send button is clicked */ - data class Send(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Send(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) /** * Receive - * * @property onClick lambda be invoked when Receive button is clicked + * @property onLongClick lambda be invoked when Receive button is long clicked */ - data class Receive(override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Receive( + override val onClick: () -> Unit, + override val onLongClick: (() -> TextReference?)?, + ) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, + onLongClick = onLongClick, enabled = true, ), ) @@ -58,30 +65,30 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { /** * Sell * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Sell button is clicked */ - data class Sell(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Sell(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_sell), iconResId = R.drawable.ic_currency_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) /** * Swap * - * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed * @property onClick lambda be invoked when Swap button is clicked */ - data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + data class Swap(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, - enabled = enabled, + dimContent = dimContent, ), ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt index 02fc3679f3..dfa224df70 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt @@ -19,7 +19,7 @@ internal data class TokenDetailsDialogConfig( sealed class DialogContentConfig { - abstract val title: TextReference + abstract val title: TextReference? abstract val message: TextReference abstract val confirmButtonConfig: ButtonConfig abstract val cancelButtonConfig: ButtonConfig? @@ -31,13 +31,13 @@ internal data class TokenDetailsDialogConfig( ) data class ConfirmHideConfig( - val currencySymbol: String, + val currencyTitle: String, val onConfirmClick: () -> Unit, val onCancelClick: () -> Unit, ) : DialogContentConfig() { override val title: TextReference = TextReference.Res( id = R.string.token_details_hide_alert_title, - formatArgs = wrappedList(currencySymbol), + formatArgs = wrappedList(currencyTitle), ) override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message) @@ -77,5 +77,39 @@ internal data class TokenDetailsDialogConfig( onClick = onConfirmClick, ) } + + data class DisabledButtonReasonDialogConfig( + val text: TextReference, + val onConfirmClick: () -> Unit, + ) : DialogContentConfig() { + + override val title = null + + override val message: TextReference = text + + override val cancelButtonConfig = null + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_ok), + onClick = onConfirmClick, + ) + } + + data class ErrorDialogConfig( + val text: TextReference, + val onConfirmClick: () -> Unit, + ) : DialogContentConfig() { + + override val title = null + + override val message: TextReference = text + + override val cancelButtonConfig = null + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_ok), + onClick = onConfirmClick, + ) + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 874efc22b9..c6314d4e3f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -66,7 +66,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) - object NetworksUnreachable : Warning( + data object NetworksUnreachable : Warning( title = resourceReference(R.string.warning_network_unreachable_title), subtitle = resourceReference(R.string.warning_network_unreachable_message), ) @@ -117,8 +117,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ), iconResId = currency.networkIconResId, - buttonsState = - NotificationConfig.ButtonsState.SecondaryButtonConfig( + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference( id = R.string.common_buy_currency, formatArgs = wrappedList( @@ -167,21 +166,35 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) - object TopUpWithoutReserve : Informational( + data object TopUpWithoutReserve : Informational( title = resourceReference(id = R.string.warning_no_account_title), subtitle = resourceReference(id = R.string.no_account_send_to_create), ) - class HasPendingTransactions(val coinSymbol: String) : Informational( - title = resourceReference(R.string.warning_send_blocked_pending_transactions_title), - subtitle = resourceReference( - id = R.string.warning_send_blocked_pending_transactions_message, - formatArgs = wrappedList(coinSymbol), - ), - ) - data class NetworkShutdown(private val title: TextReference, private val subtitle: TextReference) : Warning( title = title, subtitle = subtitle, ) + + data class HederaAssociateWarning( + private val currency: CryptoCurrency, + private val fee: String?, + private val feeCurrencySymbol: String?, + private val onAssociateClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.warning_hedera_missing_token_association_title), + subtitle = if (fee != null && feeCurrencySymbol != null) { + resourceReference( + id = R.string.warning_hedera_missing_token_association_message, + formatArgs = wrappedList(fee, feeCurrencySymbol), + ) + } else { + resourceReference(R.string.warning_hedera_missing_token_association_message_brief) + }, + iconResId = currency.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_hedera_missing_token_association_button_title), + onClick = onAssociateClick, + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt index 956cdaf6d5..dbf8a99f3c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton @@ -26,19 +27,34 @@ internal class TokenDetailsActionButtonsConverter( .mapNotNull { action -> when (action) { is TokenActionsState.ActionState.Buy -> { - TokenDetailsActionButton.Buy(enabled = action.enabled, onClick = clickIntents::onBuyClick) + TokenDetailsActionButton.Buy( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onBuyClick(action.unavailabilityReason) }, + ) } is TokenActionsState.ActionState.Receive -> { - TokenDetailsActionButton.Receive(onClick = clickIntents::onReceiveClick) + TokenDetailsActionButton.Receive( + onClick = { clickIntents.onReceiveClick(action.unavailabilityReason) }, + onLongClick = clickIntents::onCopyAddress, + ) } is TokenActionsState.ActionState.Sell -> { - TokenDetailsActionButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) + TokenDetailsActionButton.Sell( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onSellClick(action.unavailabilityReason) }, + ) } is TokenActionsState.ActionState.Send -> { - TokenDetailsActionButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick) + TokenDetailsActionButton.Send( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onSendClick(action.unavailabilityReason) }, + ) } is TokenActionsState.ActionState.Swap -> { - TokenDetailsActionButton.Swap(enabled = action.enabled, onClick = clickIntents::onSwapClick) + TokenDetailsActionButton.Swap( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onSwapClick(action.unavailabilityReason) }, + ) } else -> { null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 5048215ac6..63d735d3ad 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -84,7 +84,7 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun getMarketPriceState( - status: CryptoCurrencyStatus.Status, + status: CryptoCurrencyStatus.Value, currencySymbol: String, ): MarketPriceBlockState { return when (status) { @@ -106,7 +106,7 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun CryptoCurrencyStatus.Status.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content { + private fun CryptoCurrencyStatus.Value.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content { return MarketPriceBlockState.Content( currencySymbol = currencySymbol, price = formatPrice(status = this, appCurrency = appCurrencyProvider()), @@ -117,11 +117,11 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + private fun getPriceChangeType(status: CryptoCurrencyStatus.Value): PriceChangeType { return PriceChangeConverter.fromBigDecimal(status.priceChange) } - private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN return BigDecimalFormatter.formatPercent( @@ -130,17 +130,17 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - return BigDecimalFormatter.formatFiatAmount( + return BigDecimalFormatter.formatFiatAmountUncapped( fiatAmount = fiatRate, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) } - private fun formatFiatAmount(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN return BigDecimalFormatter.formatFiatAmount( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 23080d5d8e..33c5b85d67 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -1,10 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.model.warnings.HederaWarnings import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.* @@ -29,6 +31,13 @@ internal class TokenDetailsNotificationConverter( return newNotifications.toImmutableList() } + fun removeHederaAssociateWarning(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is HederaAssociateWarning } + return newNotifications.toImmutableList() + } + + @Suppress("LongMethod") private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification { return when (warning) { is CryptoCurrencyWarning.BalanceNotEnoughForFee -> NetworkFeeWithBuyButton( @@ -67,13 +76,14 @@ internal class TokenDetailsNotificationConverter( CryptoCurrencyWarning.SomeNetworksUnreachable -> NetworksUnreachable is CryptoCurrencyWarning.SomeNetworksNoAccount -> NetworksNoAccount( network = warning.amountCurrency.name, - amount = warning.amountToCreateAccount.toString(), + amount = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = warning.amountToCreateAccount, + cryptoCurrency = "", + decimals = warning.amountCurrency.decimals, + ), symbol = warning.amountCurrency.symbol, ) is CryptoCurrencyWarning.TopUpWithoutReserve -> TopUpWithoutReserve - is CryptoCurrencyWarning.HasPendingTransactions -> HasPendingTransactions( - coinSymbol = warning.blockchainSymbol, - ) is CryptoCurrencyWarning.SwapPromo -> SwapPromo( startDateTime = warning.startDateTime, endDateTime = warning.endDateTime, @@ -84,6 +94,23 @@ internal class TokenDetailsNotificationConverter( title = resourceReference(R.string.warning_beacon_chain_retirement_title), subtitle = resourceReference(R.string.warning_beacon_chain_retirement_content), ) + + is HederaWarnings.AssociateWarning -> HederaAssociateWarning( + currency = warning.currency, + fee = null, + feeCurrencySymbol = null, + onAssociateClick = clickIntents::onAssociateClick, + ) + is HederaWarnings.AssociateWarningWithFee -> HederaAssociateWarning( + currency = warning.currency, + fee = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = warning.fee, + cryptoCurrency = "", + decimals = warning.feeCurrencyDecimals, + ), + feeCurrencySymbol = warning.feeCurrencySymbol, + onAssociateClick = clickIntents::onAssociateClick, + ) } } 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..073b8e88ba 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.Receive(onClick = {}), - TokenDetailsActionButton.Sell(enabled = false, onClick = {}), - TokenDetailsActionButton.Swap(enabled = false, onClick = {}), + TokenDetailsActionButton.Buy(dimContent = false, onClick = {}), + TokenDetailsActionButton.Send(dimContent = false, onClick = {}), + TokenDetailsActionButton.Receive(onClick = {}, onLongClick = null), + TokenDetailsActionButton.Sell(dimContent = false, onClick = {}), + TokenDetailsActionButton.Swap(dimContent = false, onClick = {}), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 06b0c838c7..1cd62078a1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -12,14 +12,12 @@ import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkAddress -import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError @@ -40,7 +38,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, @@ -155,7 +153,7 @@ internal class TokenDetailsStateFactory( isShow = true, onDismissRequest = clickIntents::onDismissDialog, content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig( - currencySymbol = currency.symbol, + currencyTitle = currency.name, onConfirmClick = clickIntents::onHideConfirmed, onCancelClick = clickIntents::onDismissDialog, ), @@ -177,6 +175,32 @@ internal class TokenDetailsStateFactory( ) } + fun getStateWithActionButtonErrorDialog(unavailabilityReason: ScenarioUnavailabilityReason): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.DisabledButtonReasonDialogConfig( + text = getUnavailabilityReasonText(unavailabilityReason), + onConfirmClick = clickIntents::onDismissDialog, + ), + ), + ) + } + + fun getStateWithErrorDialog(text: TextReference): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ErrorDialogConfig( + text = text, + onConfirmClick = clickIntents::onDismissDialog, + ), + ), + ) + } + fun getRefreshingState(): TokenDetailsState { return refreshStateConverter.convert(true) } @@ -246,6 +270,11 @@ internal class TokenDetailsStateFactory( return state.copy(notifications = notificationConverter.removeRentInfo(state)) } + fun getStateWithRemovedHederaAssociateNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeHederaAssociateWarning(state)) + } + fun getStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TokenDetailsState { return currentStateProvider().copy( bottomSheetConfig = TangemBottomSheetConfig( @@ -321,4 +350,60 @@ internal class TokenDetailsStateFactory( }.toImmutableList(), ) } + + private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { + return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.PendingTransaction -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_send, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + } + } + is ScenarioUnavailabilityReason.EmptyBalance -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_send, + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_sell, + ) + } + } + is ScenarioUnavailabilityReason.BuyUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_buy_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotExchangeable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_not_exchangeable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotSupportedBySellService -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.Unreachable -> { + resourceReference( + id = R.string.token_button_unavailability_generic_description, + ) + } + ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( + id = R.string.warning_receive_blocked_hedera_token_association_required_message, + ) + ScenarioUnavailabilityReason.None -> { + throw IllegalArgumentException("The unavailability reason must be other than None") + } + } + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 070d2b0065..ebce26c35c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box @@ -39,6 +40,7 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification @@ -184,21 +186,12 @@ internal fun TokenDetailsEventEffect(snackbarHostState: SnackbarHostState, event // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TokenDetailsScreenPreview_Light( +private fun TokenDetailsScreenPreview( @PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState, ) { - TangemTheme { - TokenDetailsScreen(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun TokenDetailsScreenPreview_Dark( - @PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokenDetailsScreen(state) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index e8fd329b15..3f448ae33a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -14,6 +15,7 @@ import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState @@ -129,21 +131,12 @@ private fun CryptoBalance( } @Preview(widthDp = 328, heightDp = 152) +@Preview(widthDp = 328, heightDp = 152, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenDetailsBalanceBlock_LightTheme( +private fun Preview_TokenDetailsBalanceBlock( @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, ) { - TangemTheme(isDark = false) { - TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) - } -} - -@Preview(widthDp = 328, heightDp = 152) -@Composable -private fun Preview_TokenDetailsBalanceBlock_DarkTheme( - @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt index 174eb35567..ed54c9459b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -25,7 +25,7 @@ private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { onClick = config.content.confirmButtonConfig.onClick, ), onDismissDialog = config.onDismissRequest, - title = config.content.title.resolveReference(), + title = config.content.title?.resolveReference(), dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> DialogButton( title = cancelButtonConfig.text.resolveReference(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt index 1ed6603ba3..3ea60b1140 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -18,6 +19,7 @@ import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig @@ -96,9 +98,10 @@ private fun AppBarDropdownItem( } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() { - TangemTheme(isDark = false) { +private fun Preview_TokenDetailsAppBarDropdownItem() { + TangemThemePreview { AppBarDropdownItem( modifier = Modifier.background(TangemTheme.colors.background.primary), dismissParent = {}, @@ -112,33 +115,10 @@ private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() { } @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenDetailsAppBarDropdownItem_DarkTheme() { - TangemTheme(isDark = true) { - AppBarDropdownItem( - modifier = Modifier.background(TangemTheme.colors.background.primary), - dismissParent = {}, - item = TokenDetailsAppBarMenuConfig.MenuItem( - title = TextReference.Res(id = R.string.token_details_hide_token), - textColorProvider = { TangemTheme.colors.text.warning }, - onClick = { }, - ), - ) - } -} - -@Preview -@Composable -private fun Preview_TokenDetailsTopAppBar_LightTheme() { - TangemTheme(isDark = false) { - TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) - } -} - -@Preview -@Composable -private fun Preview_TokenDetailsTopAppBar_DarkTheme() { - TangemTheme(isDark = true) { +private fun Preview_TokenDetailsTopAppBar() { + TangemThemePreview { TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 3982a8dc21..37f036d60f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.Text @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.features.tokendetails.impl.R @@ -136,23 +138,13 @@ private val GrayscaleColorFilter: ColorFilter get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_TokenInfoBlock_LightTheme( +private fun Preview_TokenInfoBlock( @PreviewParameter(TokenInfoStateProvider::class) state: TokenInfoBlockState, ) { - TangemTheme(isDark = false) { - TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) - } -} - -@Preview -@Composable -private fun Preview_TokenInfoBlock_DarkTheme( - @PreviewParameter(TokenInfoStateProvider::class) - state: TokenInfoBlockState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt index 5f3527a8eb..7d1c2f14b5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt @@ -22,6 +22,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.InputRowBestRate import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview @Composable internal fun ExchangeProvider( @@ -90,7 +91,7 @@ internal fun ExchangeProvider( @Preview(showBackground = true) @Composable private fun ExchangeProvider_Preview() { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { ExchangeProvider( providerName = TextReference.Str("Changelly"), providerType = TextReference.Str("CEX"), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 53e398e71f..1945621d19 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange +import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -25,6 +26,7 @@ import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.features.tokendetails.impl.R @@ -195,31 +197,12 @@ private fun ExchangeStatusItem( //region Preview @Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ExchangeStatusItemPreview_Light( +private fun ExchangeStatusItemPreview( @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String, ) { - TangemTheme { - ExchangeStatusItem( - providerName = "ChangeNow", - fromTokenIconState = TokenIconState.Loading, - toTokenIconState = TokenIconState.Loading, - fromAmount = amount, - fromSymbol = "USDT", - toSymbol = "USDT", - onClick = {}, - infoIconRes = null, - infoIconTint = null, - ) - } -} - -@Preview -@Composable -private fun ExchangeStatusItemPreview_Dark( - @PreviewParameter(ExchangeStatusItemsPreviewParameterProvider::class) amount: String, -) { - TangemTheme(isDark = true) { + TangemThemePreview { ExchangeStatusItem( providerName = "ChangeNow", fromTokenIconState = TokenIconState.Loading, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 712afdeb80..751b8dcd92 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -1,20 +1,24 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason @Suppress("TooManyFunctions") interface TokenDetailsClickIntents { fun onBackClick() - fun onSendClick() + fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) - fun onReceiveClick() + fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) - fun onSellClick() + fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) - fun onSwapClick() + fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) + + fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) fun onDismissDialog() @@ -24,8 +28,6 @@ interface TokenDetailsClickIntents { fun onRefreshSwipe() - fun onBuyClick() - fun onBuyCoinClick(cryptoCurrency: CryptoCurrency) fun onReloadClick() @@ -49,4 +51,8 @@ interface TokenDetailsClickIntents { fun onSwapPromoClick() fun onGenerateExtendedKey() + + fun onCopyAddress(): TextReference? + + fun onAssociateClick() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 02defbde3a..814cc1bcdd 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 @@ -10,9 +10,14 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.BuyCurrencyDeepLink +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.extensions.wrappedList import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -28,14 +33,18 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.error.AssociateAssetError +import com.tangem.domain.transaction.usecase.AssociateAssetUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -58,6 +67,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* @@ -70,7 +80,6 @@ import javax.inject.Inject @HiltViewModel internal class TokenDetailsViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, - private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -93,8 +102,12 @@ internal class TokenDetailsViewModel @Inject constructor( private val quotesRepository: QuotesRepository, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val isDemoCardUseCase: IsDemoCardUseCase, + private val associateAssetUseCase: AssociateAssetUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, + private val hapticManager: HapticManager, + private val clipboardManager: ClipboardManager, + getUserWalletUseCase: GetUserWalletUseCase, featureToggles: TokenDetailsFeatureToggles, deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, @@ -107,6 +120,8 @@ internal class TokenDetailsViewModel @Inject constructor( private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] ?: error("This screen can't open without `CryptoCurrency`") + private val userWallet: UserWallet + lateinit var router: InnerTokenDetailsRouter private val marketPriceJobHolder = JobHolder() @@ -166,6 +181,7 @@ internal class TokenDetailsViewModel @Inject constructor( BuyCurrencyDeepLink(::onBuyCurrencyDeepLink), ), ) + userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found") } private fun onBuyCurrencyDeepLink() { @@ -204,8 +220,7 @@ internal class TokenDetailsViewModel @Inject constructor( .launchIn(viewModelScope) } - private suspend fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { return } + private suspend fun updateButtons(currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = currencyStatus, @@ -219,12 +234,11 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateWarnings(cryptoCurrencyStatus: CryptoCurrencyStatus) { viewModelScope.launch(dispatchers.io) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } getCurrencyWarningsUseCase.invoke( userWalletId = userWalletId, currencyStatus = cryptoCurrencyStatus, derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .distinctUntilChanged() .onEach { @@ -239,18 +253,17 @@ internal class TokenDetailsViewModel @Inject constructor( private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch(dispatchers.io) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } getCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .distinctUntilChanged() .onEach { maybeCurrencyStatus -> uiState = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) maybeCurrencyStatus.onRight { status -> cryptoCurrencyStatus = status - updateButtons(userWalletId = userWalletId, currencyStatus = status) + updateButtons(currencyStatus = status) updateWarnings(status) } currencyStatusAnalyticsSender.send(maybeCurrencyStatus) @@ -347,10 +360,8 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateTopBarMenu() { viewModelScope.launch(dispatchers.main) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } - uiState = stateFactory.getStateWithUpdatedMenu( - cardTypesResolver = wallet.scanResponse.cardTypesResolver, + cardTypesResolver = userWallet.scanResponse.cardTypesResolver, isBitcoin = isBitcoin(cryptoCurrency.network.id.value), ) } @@ -372,16 +383,18 @@ internal class TokenDetailsViewModel @Inject constructor( router.popBackStack() } - override fun onBuyClick() { + override fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonBuy(cryptoCurrency.symbol)) + if (handleUnavailabilityReason(unavailabilityReason)) return + showErrorIfDemoModeOrElse { val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( TradeCryptoAction.Buy( - userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + userWallet = userWallet, cryptoCurrencyStatus = status, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), @@ -401,9 +414,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) } @@ -416,7 +431,7 @@ internal class TokenDetailsViewModel @Inject constructor( is CryptoCurrency.Coin -> { reduxStateHolder.dispatch( action = TradeCryptoAction.SendCoin( - userWallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }, + userWallet = userWallet, coinStatus = status, feeCurrencyStatus = maybeFeeCurrencyStatus, transactionInfo = transactionInfo, @@ -442,12 +457,11 @@ internal class TokenDetailsViewModel @Inject constructor( transactionInfo: TransactionInfo?, ) { viewModelScope.launch(dispatchers.io) { - val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } val maybeCoinStatus = getNetworkCoinStatusUseCase( userWalletId = userWalletId, networkId = tokenCurrency.network.id, derivationPath = tokenCurrency.network.derivationPath, - isSingleWalletWithTokens = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .conflate() .distinctUntilChanged() @@ -455,7 +469,7 @@ internal class TokenDetailsViewModel @Inject constructor( reduxStateHolder.dispatchWithMain( action = TradeCryptoAction.SendToken( - userWallet = wallet, + userWallet = userWallet, tokenCurrency = tokenCurrency, tokenFiatRate = tokenFiatRate, coinFiatRate = maybeCoinStatus?.fold( @@ -469,9 +483,11 @@ internal class TokenDetailsViewModel @Inject constructor( } } - override fun onReceiveClick() { + override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) { val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return + if (handleUnavailabilityReason(unavailabilityReason)) return + viewModelScope.launch(dispatchers.io) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened) @@ -507,9 +523,11 @@ internal class TokenDetailsViewModel @Inject constructor( } } - override fun onSellClick() { + override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrency.symbol)) + if (handleUnavailabilityReason(unavailabilityReason)) return + showErrorIfDemoModeOrElse { val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse @@ -522,9 +540,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)) } @@ -582,9 +602,7 @@ internal class TokenDetailsViewModel @Inject constructor( private fun showErrorIfDemoModeOrElse(action: () -> Unit) { viewModelScope.launch(dispatchers.main) { - val wallet = getUserWalletUseCase(userWalletId = userWalletId).getOrElse { return@launch } - - if (isDemoCardUseCase(cardId = wallet.cardId)) { + if (isDemoCardUseCase(cardId = userWallet.cardId)) { uiState = stateFactory.getStateWithClosedBottomSheet() uiState = stateFactory.getStateAndTriggerEvent( state = uiState, @@ -681,7 +699,56 @@ internal class TokenDetailsViewModel @Inject constructor( shouldShowSwapPromoTokenUseCase.neverToShow() analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) } - onSwapClick() + onSwapClick(ScenarioUnavailabilityReason.None) + } + + override fun onCopyAddress(): TextReference? { + val networkAddress = cryptoCurrencyStatus?.value?.networkAddress ?: return null + val addresses = networkAddress.availableAddresses.mapToAddressModels(cryptoCurrency).toImmutableList() + val defaultAddress = addresses.firstOrNull()?.value ?: return null + + hapticManager.vibrateMeduim() + clipboardManager.setText(text = defaultAddress) + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + return resourceReference(R.string.wallet_notification_address_copied) + } + + override fun onAssociateClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.Associate( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + viewModelScope.launch(dispatchers.io) { + associateAssetUseCase( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifLeft = { e -> + when (e) { + is AssociateAssetError.NotEnoughBalance -> { + uiState = stateFactory.getStateWithErrorDialog( + resourceReference( + id = R.string.warning_hedera_token_association_not_enough_hbar_message, + formatArgs = wrappedList(e.feeCurrency.symbol), + ), + ) + } + is AssociateAssetError.DataError -> Timber.e(e.message) + } + }, + ifRight = { uiState = stateFactory.getStateWithRemovedHederaAssociateNotification() }, + ) + } + } + + 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/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 74df363767..5a2c923015 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.wallet.navigation.WalletRouter @@ -21,7 +21,7 @@ import javax.inject.Inject internal class WalletFragment : ComposeFragment() { @Inject - override lateinit var appThemeModeHolder: AppThemeModeHolder + override lateinit var uiDependencies: UiDependencies @Inject internal lateinit var manageTokensUi: ManageTokensUi diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt index c5af5a2cf6..c0d0a73b1a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.common.component +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -13,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder @@ -92,17 +94,10 @@ private fun NetworkGroupItemSample(isDraggable: Boolean) { } @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun NetworkGroupItemPreview_Light(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) { - TangemTheme { - NetworkGroupItemSample(isDraggable) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun NetworkGroupItemPreview_Dark(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) { - TangemTheme(isDark = true) { +private fun NetworkGroupItemPreview(@PreviewParameter(NetworkGroupProvider::class) isDraggable: Boolean) { + TangemThemePreview { NetworkGroupItemSample(isDraggable) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 0b20c2a43a..989965b112 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.token.* import com.tangem.feature.wallet.presentation.common.state.TokenItemState @@ -392,7 +393,7 @@ private fun calculateLayoutHeight( @Preview(widthDp = 360) @Composable private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { - TangemTheme(isDark = false) { + TangemThemePreview(isDark = false) { TokenItem(state = state, isBalanceHidden = false) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 294fe204cd..4efcf827cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens +import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background @@ -34,6 +35,7 @@ import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem @@ -373,21 +375,12 @@ private fun getItemShape(roundingMode: DraggableItem.RoundingMode, radius: Dp): // region Preview @Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun OrganizeTokensScreenPreview_Light( +private fun OrganizeTokensScreenPreview( @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState, ) { - TangemTheme { - OrganizeTokensScreen(state) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun OrganizeTokensScreenPreview_Dark( - @PreviewParameter(OrganizeTokensStateProvider::class) state: OrganizeTokensState, -) { - TangemTheme(isDark = true) { + TangemThemePreview { OrganizeTokensScreen(state) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 5342e0ece5..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..f0ca57a2b3 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 @@ -33,7 +33,7 @@ internal class TokenListAnalyticsSender @Inject constructor( private val mutex = Mutex() suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) { - if (screenLifecycleProvider.isBackground) return + if (screenLifecycleProvider.isBackgroundState.value) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return if (tokenList.totalFiatBalance is TokenList.FiatBalance.Loading) return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 16ba03cb43..59d4331387 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -18,7 +18,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( ) { fun send(displayedUiState: WalletState?, newWarnings: List) { - if (screenLifecycleProvider.isBackground) return + if (screenLifecycleProvider.isBackgroundState.value) return if (newWarnings.isEmpty()) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index daaebb07e1..a2adf6b292 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -47,7 +47,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) } return combine( - flow = getTokenListUseCase(userWallet.walletId).conflate(), + flow = getTokenListUseCase.launch(userWallet.walletId).conflate(), flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), flow4 = shouldShowTravalaPromoWalletUseCase().conflate(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt index 1a4a34780d..e528262365 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt @@ -6,7 +6,8 @@ import arrow.core.raise.ensure import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.SaveWalletUseCase import javax.inject.Inject @@ -14,6 +15,7 @@ import javax.inject.Inject internal class ScanCardToUnlockWalletClickHandler @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, ) { private var scanFailsCounter = 0 @@ -31,7 +33,7 @@ internal class ScanCardToUnlockWalletClickHandler @Inject constructor( scanFailsCounter = 0 // If card's public key is null then user wallet will be null - val scannedWallet = UserWalletBuilder(scanResponse = result.data).build() + val scannedWallet = UserWalletBuilder(result.data, generateWalletNameUseCase).build() ensure(walletId == scannedWallet?.walletId) { ScanCardToUnlockWalletError.WrongCardIsScanned diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt new file mode 100644 index 0000000000..99e60b3682 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository +import timber.log.Timber + +class WalletNameMigrationUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val walletNamesMigrationRepository: WalletNamesMigrationRepository, +) { + + suspend operator fun invoke() { + val wallets = userWalletsListManager.userWalletsSync + + if (walletNamesMigrationRepository.isMigrationDone()) { + return + } + + val existingNames: MutableSet = mutableSetOf() + wallets.indices.forEach { i -> + val defaultName = wallets[i].name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + } + Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) + } + + walletNamesMigrationRepository.setMigrationDone() + } + + private fun suggestedWalletName(defaultName: String, existingNames: MutableSet): String { + val startIndex = 1 + for (index in startIndex..MAX_WALLETS_LIMIT) { + val name = if (index == startIndex) defaultName else "$defaultName $index" + + if (!existingNames.contains(name)) { + existingNames.add(name) + return name + } + } + + return defaultName + } + + companion object { + const val MAX_WALLETS_LIMIT = 10000 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index bc6d40794b..0a3310ce92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -1,11 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory @@ -13,7 +13,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsCheck import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletConnectNetworksSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents @@ -29,7 +28,7 @@ internal class MultiWalletContentLoader( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val reduxStateHolder: ReduxStateHolder, + private val walletFeatureToggles: WalletFeatureToggles, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -43,6 +42,7 @@ internal class MultiWalletContentLoader( walletWithFundsChecker = walletWithFundsChecker, getTokenListUseCase = getTokenListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + walletFeatureToggles = walletFeatureToggles, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), @@ -53,11 +53,6 @@ internal class MultiWalletContentLoader( getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, ), - WalletConnectNetworksSubscriber( - userWallet = userWallet, - getTokenListUseCase = getTokenListUseCase, - reduxStateHolder = reduxStateHolder, - ), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 540fdea589..13dbadebb3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -1,11 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory @@ -25,8 +25,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val reduxStateHolder: ReduxStateHolder, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, + private val walletFeatureToggles: WalletFeatureToggles, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) { @@ -40,9 +40,9 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( getTokenListUseCase = getTokenListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - reduxStateHolder = reduxStateHolder, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, applyTokenListSortingUseCase = applyTokenListSortingUseCase, + walletFeatureToggles = walletFeatureToggles, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index b8aecf2d9f..03fb7f7902 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -25,10 +25,11 @@ internal sealed interface WalletAlertState { open val text: String = "" open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) abstract val onConfirmClick: (String) -> Unit + abstract val errorTextProvider: (String) -> TextReference? } data class DefaultAlert( - override val title: TextReference, + override val title: TextReference?, override val message: TextReference, override val onConfirmClick: (() -> Unit)?, ) : Basic() @@ -36,6 +37,7 @@ internal sealed interface WalletAlertState { data class RenameWalletAlert( override val text: String, override val onConfirmClick: (String) -> Unit, + override val errorTextProvider: (String) -> TextReference?, ) : TextInput() { override val title: TextReference = resourceReference(id = R.string.user_wallet_list_rename_popup_title) override val label: TextReference = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt index 5ee98fda79..50a1af41c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt @@ -10,11 +10,9 @@ internal sealed class WalletEvent { data class ShowError(val text: TextReference) : WalletEvent() - data class ShowToast(val text: TextReference) : WalletEvent() - data class ShowAlert(val state: WalletAlertState) : WalletEvent() - data class CopyAddress(val address: String, val toast: TextReference) : WalletEvent() + data class CopyAddress(val address: String) : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index 124d3bf6a9..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/model/WalletPullToRefreshConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt index ca91846374..8bf0e4ea63 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletPullToRefreshConfig.kt @@ -6,4 +6,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model * @property isRefreshing state is indicator visible * @property onRefresh lambda be invoked when pulled to refresh */ -data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file +data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: (ShowRefreshState) -> Unit) { + + @JvmInline + value class ShowRefreshState( + val value: Boolean, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/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..8d710656dc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -10,7 +10,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.converter.Converter internal class SingleWalletMarketPriceConverter( - private val status: CryptoCurrencyStatus.Status, + private val status: CryptoCurrencyStatus.Value, private val appCurrency: AppCurrency, ) : Converter { @@ -44,23 +44,23 @@ internal class SingleWalletMarketPriceConverter( ) } - private fun formatPrice(status: CryptoCurrencyStatus.Status, appCurrency: AppCurrency): String { + private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - return BigDecimalFormatter.formatFiatAmount( + return BigDecimalFormatter.formatFiatAmountUncapped( fiatAmount = fiatRate, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) } - private fun formatPriceChange(status: CryptoCurrencyStatus.Status): String { + private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true) } - private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType { + private fun getPriceChangeType(status: CryptoCurrencyStatus.Value): PriceChangeType { return PriceChangeConverter.fromBigDecimal(status.priceChange) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index aa168db655..f41d4d4ee7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -108,7 +108,7 @@ internal class TokenItemStateConverter( private fun BigDecimal.getFormattedCryptoPrice(): String { val appCurrency = appCurrencyProvider() - return BigDecimalFormatter.formatFiatAmount( + return BigDecimalFormatter.formatFiatAmountUncapped( fiatAmount = this, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 866f9981a0..71f0bdd4c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -45,7 +45,7 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn walletCardState = userWallet.toLoadingWalletCardState(), warnings = persistentListOf(), bottomSheetConfig = null, - buttons = createDisabledButtons(), + buttons = createDimmedButtons(), marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = currencySymbol), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( @@ -72,7 +72,7 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn } private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { - return WalletPullToRefreshConfig(onRefresh = clickIntents::onRefreshSwipe, isRefreshing = false) + return WalletPullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) } private fun UserWallet.toLoadingWalletCardState(): WalletCardState { @@ -86,12 +86,12 @@ internal class WalletLoadingStateFactory(private val clickIntents: WalletClickIn ) } - private fun createDisabledButtons(): PersistentList { + private fun createDimmedButtons(): PersistentList { return persistentListOf( - WalletManageButton.Buy(enabled = false, onClick = {}), - WalletManageButton.Send(enabled = false, onClick = {}), - WalletManageButton.Receive(enabled = false, onClick = {}), - WalletManageButton.Sell(enabled = false, onClick = {}), + WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}), + 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/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt index c5303e085e..48e959ad54 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt @@ -64,7 +64,9 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U fieldValue = value, confirmButton = DialogButton( title = state.confirmButtonText.resolveReference(), - enabled = value.text.isNotEmpty() && value.text != state.text, + enabled = value.text.isNotEmpty() && + value.text != state.text && + state.errorTextProvider(value.text) == null, onClick = { state.onConfirmClick(value.text) onDismiss() @@ -76,6 +78,8 @@ private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> U dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss), textFieldParams = AdditionalTextInputDialogParams( label = state.label.resolveReference(), + isError = state.errorTextProvider(value.text) != null, + caption = state.errorTextProvider(value.text)?.resolveReference(), ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index cce277150d..19cd88f97f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui -import android.widget.Toast import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -11,6 +11,7 @@ import androidx.compose.ui.text.AnnotatedString import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester @@ -42,12 +43,12 @@ internal fun WalletEventEffect( is WalletEvent.ShowError -> { snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) } - is WalletEvent.ShowToast -> { - Toast.makeText(context, value.text.resolveReference(resources), Toast.LENGTH_SHORT).show() - } is WalletEvent.CopyAddress -> { clipboardManager.setText(AnnotatedString(value.address)) - Toast.makeText(context, value.toast.resolveReference(resources), Toast.LENGTH_SHORT).show() + snackbarHostState.showSnackbar( + message = resources.getString(R.string.wallet_notification_address_copied), + duration = SnackbarDuration.Short, + ) } is WalletEvent.ShowAlert -> onAlertConfigSet(value.state) is WalletEvent.RateApp -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 9819f09790..539e8e9859 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 @@ -37,8 +38,12 @@ import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBot import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar +import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.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 +138,9 @@ 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, @@ -284,7 +291,11 @@ private fun BaseScaffoldManageTokenRedesign( BottomSheetScaffold( snackbarHost = { - SnackbarHost(hostState = snackbarHostState) + WalletSnackbarHost( + snackbarHostState = it, + event = state.event, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + ) }, containerColor = TangemTheme.colors.background.secondary, sheetContainerColor = TangemTheme.colors.background.primary, @@ -315,7 +326,9 @@ private fun BaseScaffoldManageTokenRedesign( content = { paddingValues -> val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, ) Column( @@ -486,7 +499,13 @@ private fun BaseScaffold( ) { Scaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + snackbarHost = { + WalletSnackbarHost( + snackbarHostState = snackbarHostState, + event = state.event, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + ) + }, floatingActionButton = { val manageTokensButtonConfig by remember(state.selectedWalletIndex) { mutableStateOf( @@ -501,7 +520,9 @@ private fun BaseScaffold( content = { val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = selectedWallet.pullToRefreshConfig.onRefresh, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, ) Box( @@ -521,6 +542,21 @@ private fun BaseScaffold( ) } +@Composable +private fun WalletSnackbarHost( + snackbarHostState: SnackbarHostState, + event: StateEvent, + modifier: Modifier = Modifier, +) { + SnackbarHost(hostState = snackbarHostState, modifier = modifier) { data -> + if (event is StateEvent.Triggered && event.data is WalletEvent.CopyAddress) { + CopiedTextSnackbar(data) + } else { + TangemSnackbar(data) + } + } +} + @Composable private fun ManageTokensButton(onClick: () -> Unit) { PrimaryButton( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index 865a52268f..5e4db0ab08 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable @@ -14,6 +15,7 @@ import com.tangem.core.ui.components.getDefaultRowColors import com.tangem.core.ui.components.getWarningRowColors import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig @@ -49,24 +51,13 @@ private fun ActionsBottomSheetContent(actions: ImmutableList = _isBackgroundState override fun onResume(owner: LifecycleOwner) { - isBackground = false + _isBackgroundState.value = false } override fun onPause(owner: LifecycleOwner) { - isBackground = true + _isBackgroundState.value = true } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 369ec32691..44d391e849 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -5,23 +5,23 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.AppScreen import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase -import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender @@ -32,6 +32,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.extensions.indexOfFirstOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -56,25 +57,35 @@ internal class WalletViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: ReduxStateHolder, private val screenLifecycleProvider: ScreenLifecycleProvider, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, + private val walletNameMigrationUseCase: WalletNameMigrationUseCase, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState private lateinit var router: InnerWalletRouter - private var walletsUpdateJobHolder: JobHolder = JobHolder() + private val walletsUpdateJobHolder = JobHolder() + private val refreshWalletJobHolder = JobHolder() + private var needToRefreshWallet = false init { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) suggestToEnableBiometrics() + maybeMigrateNames() subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() + subscribeToScreenBackgroundState() + } + + private fun maybeMigrateNames() { + viewModelScope.launch { + walletNameMigrationUseCase() + } } fun setWalletRouter(router: InnerWalletRouter) { @@ -141,29 +152,62 @@ internal class WalletViewModel @Inject constructor( .distinctUntilChanged() .onEach { selectedWallet -> if (selectedWallet.isMultiCurrency) { - Timber.d("WalletConnect: initialize and setup networks for ${selectedWallet.walletId}") - - reduxStateHolder.dispatch( - action = WalletConnectActions.New.Initialize(userWallet = selectedWallet), - ) - - reduxStateHolder.dispatch( - action = WalletConnectActions.New.SetupUserChains(userWallet = selectedWallet), - ) - selectedWalletAnalyticsSender.send(selectedWallet) } - walletDeepLinksHandler.registerForWallet( - viewModel = this, - userWallet = selectedWallet, - ) + changeSelectedWalletState(selectedWalletId = selectedWallet.walletId) + + walletDeepLinksHandler.registerForWallet(viewModel = this, userWallet = selectedWallet) } .flowOn(dispatchers.main) .launchIn(viewModelScope) } } + /** Change selected wallet state if selected wallet [selectedWalletId] was changed in the background */ + private suspend fun changeSelectedWalletState(selectedWalletId: UserWalletId) { + if (screenLifecycleProvider.isBackgroundState.value && selectedWalletId != stateHolder.getSelectedWalletId()) { + stateHolder.value.wallets + .indexOfFirstOrNull { prevState -> prevState.walletCardState.id == selectedWalletId } + ?.let { selectedIndex -> + Timber.e("Selected wallet changed from background state: $selectedWalletId") + + delay(timeMillis = 1000) + scrollToWallet(selectedIndex) + } + } + } + + // We need to update the current wallet if the application was in the background for more than 10 seconds + // and then returned to the foreground + private fun subscribeToScreenBackgroundState() { + screenLifecycleProvider.isBackgroundState + .onEach { isBackground -> + refreshWalletJobHolder.cancel() + when { + isBackground -> needToRefreshTimer() + needToRefreshWallet && !isBackground -> triggerRefreshWallet() + } + } + .launchIn(viewModelScope) + } + + private fun needToRefreshTimer() { + viewModelScope.launch { + delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) + needToRefreshWallet = true + }.saveIn(refreshWalletJobHolder) + } + + private fun triggerRefreshWallet() { + needToRefreshWallet = false + val state = stateHolder.uiState.value + val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return + wallet.pullToRefreshConfig.onRefresh.invoke( + WalletPullToRefreshConfig.ShowRefreshState(false), + ) + } + private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) { when (action) { is WalletsUpdateActionResolver.Action.InitializeWallets -> initializeWallets(action) @@ -177,9 +221,9 @@ internal class WalletViewModel @Inject constructor( is WalletsUpdateActionResolver.Action.UpdateWalletName -> { stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name)) } - is WalletsUpdateActionResolver.Action.NoAccessibleWallets -> closeScreen(screen = AppScreen.Welcome) - is WalletsUpdateActionResolver.Action.NoWallets -> closeScreen(screen = AppScreen.Home) - is WalletsUpdateActionResolver.Action.Unknown -> Unit + is WalletsUpdateActionResolver.Action.Unknown -> { + Timber.w("Unable to perfom action: $action") + } } } @@ -293,13 +337,6 @@ internal class WalletViewModel @Inject constructor( ) } - private fun closeScreen(screen: AppScreen) { - if (!screenLifecycleProvider.isBackground) { - stateHolder.clear() - router.popBackStack(screen = screen) - } - } - private fun scrollToWallet(index: Int, onConsume: () -> Unit = {}) { stateHolder.update( ScrollToWalletTransformer( @@ -310,4 +347,8 @@ internal class WalletViewModel @Inject constructor( ), ) } + + private companion object { + const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 94b310fd4a..737fb1a107 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import arrow.core.getOrElse import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -23,16 +24,14 @@ internal class WalletsUpdateActionResolver @Inject constructor( ) { fun resolve(wallets: List, currentState: WalletScreenState): Action { - val selectedWallet = wallets.getSelectedWallet() + val selectedWallet = getSelectedWalletSyncUseCase().getOrElse { + error("Unable to find selected wallet: $it") + } - val action = if (selectedWallet == null) { - createNoSelectedWalletAction(wallets) + val action = if (isFirstInitialization(currentState)) { + createInitializeWalletsAction(wallets, selectedWallet) } else { - if (isFirstInitialization(currentState)) { - createInitializeWalletsAction(wallets, selectedWallet) - } else { - getUpdateContentAction(currentState, wallets, selectedWallet) - } + getUpdateContentAction(currentState, wallets, selectedWallet) } Timber.d("Resolved action: $action") @@ -40,22 +39,6 @@ internal class WalletsUpdateActionResolver @Inject constructor( return action } - private fun List.getSelectedWallet(): UserWallet? { - return when { - isEmpty() -> null - size == 1 -> if (first().isLocked) null else first() - else -> getSelectedWalletSyncUseCase().fold(ifLeft = { null }, ifRight = { it }) - } - } - - private fun createNoSelectedWalletAction(wallets: List): Action { - return when { - wallets.isEmpty() -> Action.NoWallets - wallets.all(UserWallet::isLocked) -> Action.NoAccessibleWallets - else -> Action.Unknown - } - } - private fun isFirstInitialization(state: WalletScreenState): Boolean { return state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX } @@ -289,10 +272,6 @@ internal class WalletsUpdateActionResolver @Inject constructor( } } - data object NoAccessibleWallets : Action() - - data object NoWallets : Action() - data object Unknown : Action() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 10d52a0047..5b4bf78f5d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,13 +1,21 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.wallets.usecase.GetWalletNamesUseCase +import com.tangem.domain.wallets.usecase.RenameWalletUseCase +import com.tangem.feature.wallet.impl.R import com.tangem.domain.card.DeleteSavedAccessCodesUseCase +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -36,32 +44,48 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, private val walletEventSender: WalletEventSender, private val walletScreenContentLoader: WalletScreenContentLoader, + private val renameWalletUseCase: RenameWalletUseCase, + private val getWalletNamesUseCase: GetWalletNamesUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val reduxStateHolder: ReduxStateHolder, + private val reduxNavController: ReduxNavController, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents { override fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) { analyticsEventHandler.send(MainScreen.EditWalletTapped) - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.RenameWalletAlert( - text = stateHolder.getSelectedWallet().walletCardState.title, - onConfirmClick = { onRenameAfterConfirmationClick(userWalletId, it) }, + viewModelScope.launch(dispatchers.main) { + val walletNames = getWalletNamesUseCase() + val currentWalletName = stateHolder.getSelectedWallet().walletCardState.title + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.RenameWalletAlert( + text = currentWalletName, + onConfirmClick = { onRenameAfterConfirmationClick(userWalletId, it) }, + errorTextProvider = { enteredName -> + if (walletNames.contains(enteredName) && enteredName != currentWalletName) { + resourceReference( + R.string.user_wallet_list_rename_popup_error_already_exists, + wrappedList(enteredName), + ) + } else { + null + } + }, + ), ), - ), - ) + ) + } } override fun onRenameAfterConfirmationClick(userWalletId: UserWalletId, name: String) { viewModelScope.launch(dispatchers.main) { - updateWalletUseCase(userWalletId = userWalletId, update = { it.copy(name = name) }) + renameWalletUseCase(userWalletId = userWalletId, name) } } @@ -81,18 +105,26 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( viewModelScope.launch(dispatchers.main) { walletScreenContentLoader.cancel(userWalletId) - val deletedUserWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch + val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch + val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + Timber.e("Unable to delete user wallet: $it") + return@launch + } - deleteSavedAccessCodesUseCase(cardId = deletedUserWallet.cardId) - .onLeft { Timber.e(it.toString()) } + deleteSavedAccessCodesUseCase(cardId = walletToDelete.cardId).onLeft { + Timber.e("Unable to delete user wallet access code: $it") + } - deleteWalletUseCase(userWalletId) - .onRight { - getSelectedWalletSyncUseCase().getOrNull()?.let { - reduxStateHolder.onUserWalletSelected(it) - } + if (hasUserWallets) { + val selectedWallet = getSelectedWalletSyncUseCase().getOrElse { + error("Unable to find selected wallet: $it") } - .onLeft { Timber.e(it.toString()) } + + reduxStateHolder.onUserWalletSelected(selectedWallet) + } else { + stateHolder.clear() + reduxNavController.navigate(NavigationAction.PopBackTo(AppScreen.Home)) + } } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 3cf99441ab..3266406422 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -77,17 +77,17 @@ internal class WalletClickIntents @Inject constructor( } } - fun onRefreshSwipe() { + fun onRefreshSwipe(showRefreshState: Boolean) { when (stateHolder.getSelectedWallet()) { is WalletState.MultiCurrency.Content -> { analyticsEventHandler.send(PortfolioEvent.Refreshed) - refreshMultiCurrencyContent() + refreshMultiCurrencyContent(showRefreshState) } is WalletState.SingleCurrency.Content, is WalletState.Visa.Content, -> { analyticsEventHandler.send(PortfolioEvent.Refreshed) - refreshSingleCurrencyContent() + refreshSingleCurrencyContent(showRefreshState) } is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, @@ -97,14 +97,14 @@ internal class WalletClickIntents @Inject constructor( } fun onReloadClick() { - refreshSingleCurrencyContent() + refreshSingleCurrencyContent(showRefreshState = true) } - private fun refreshMultiCurrencyContent() { + private fun refreshMultiCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update( - SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) viewModelScope.launch(dispatchers.main) { @@ -126,11 +126,11 @@ internal class WalletClickIntents @Inject constructor( // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() - private fun refreshSingleCurrencyContent() { + private fun refreshSingleCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return stateHolder.update( - SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = true), + SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) viewModelScope.launch(dispatchers.main) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 243f372827..53f2f86e93 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,10 @@ 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.TextReference import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver @@ -19,6 +21,7 @@ import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.walletmanager.WalletManagersFacade @@ -43,7 +46,13 @@ 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 +62,6 @@ interface WalletCurrencyActionsClickIntents { fun onPerformHideToken(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - - fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - - fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onExploreClick() } @@ -82,13 +85,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 +129,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun sendToken( cryptoCurrency: CryptoCurrency.Token, - cryptoCurrencyStatus: CryptoCurrencyStatus.Status, + cryptoCurrencyStatus: CryptoCurrencyStatus.Value, feeCurrencyStatus: CryptoCurrencyStatus?, userWallet: UserWallet, ) { @@ -198,11 +206,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( .find { it.type == AddressType.Default } ?.value ?.let { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) + walletEventSender.send( - event = WalletEvent.CopyAddress( - address = it, - toast = resourceReference(R.string.wallet_notification_address_copied), - ), + event = WalletEvent.CopyAddress(address = it), ) } } @@ -264,7 +271,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( .fold( ifLeft = { walletEventSender.send( - event = WalletEvent.ShowToast(text = resourceReference(R.string.common_error)), + event = WalletEvent.ShowError(text = resourceReference(R.string.common_error)), ) }, ifRight = { @@ -274,11 +281,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 +303,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 +328,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 +424,80 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( action() } } + + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { + if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false + + val unavailabilityReasonText = getUnavailabilityReasonText(unavailabilityReason) + + viewModelScope.launch(dispatchers.main) { + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.DefaultAlert( + title = null, + message = unavailabilityReasonText, + onConfirmClick = null, + ), + ), + ) + } + + return true + } + + private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { + return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.PendingTransaction -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_send, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_pending_transaction_sell, + formatArgs = wrappedList(unavailabilityReason.networkName), + ) + } + } + is ScenarioUnavailabilityReason.EmptyBalance -> { + when (unavailabilityReason.withdrawalScenario) { + ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_send, + ) + ScenarioUnavailabilityReason.WithdrawalScenario.SELL -> resourceReference( + id = R.string.token_button_unavailability_reason_empty_balance_sell, + ) + } + } + is ScenarioUnavailabilityReason.BuyUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_buy_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotExchangeable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_not_exchangeable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + is ScenarioUnavailabilityReason.NotSupportedBySellService -> { + resourceReference( + id = R.string.token_button_unavailability_reason_sell_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } + ScenarioUnavailabilityReason.Unreachable -> { + resourceReference( + id = R.string.token_button_unavailability_generic_description, + ) + } + ScenarioUnavailabilityReason.UnassociatedAsset -> resourceReference( + id = R.string.warning_receive_blocked_hedera_token_association_required_message, + ) + ScenarioUnavailabilityReason.None -> { + throw IllegalArgumentException("The unavailability reason must be other than None") + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 418a4d8afd..21982d8010 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -154,7 +154,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( val event = when (error) { is UnlockWalletsError.DataError, is UnlockWalletsError.UnableToUnlockWallets, - -> WalletEvent.ShowToast(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) + -> WalletEvent.ShowError(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) is UnlockWalletsError.NoUserWalletSelected, is UnlockWalletsError.NotAllUserWalletsUnlocked, -> WalletEvent.ShowAlert(WalletAlertState.RescanWallets) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index f7ce17b6c2..ec97c63128 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -39,7 +39,6 @@ compose-lifecycle-runtime = "2.7.0" # region Other libraries amplitude = "2.36.1" -appsflyer = "6.5.1" armadillo = "0.9.0" coil = "2.1.0" compose-shimmer = "1.0.3" @@ -68,7 +67,7 @@ xmlShimmer = "1.1.3" zxingQrCode = "3.5.1" mviCore = "1.3.1" kotlinSerialization = "1.4.1" -arrow = "1.2.0" +arrow = "1.2.3" reactiveNetwork = "3.0.8" walletConnectCore = "1.18.0" walletConnectWeb3 = "1.11.0" @@ -82,13 +81,15 @@ swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" web3j = "4.10.1" leakcanary = "2.13" +decompose = "2.2.2" +room = "2.6.1" markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.10-639" +tangemBlockchainSdk = "develop-637" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.10-353" +tangemCardSdk = "develop-356" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem @@ -121,6 +122,7 @@ firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = " google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +room = { id = "androidx.room", version.ref = "room" } [libraries] # region Classpath @@ -198,11 +200,12 @@ test-kaspresso = { module = "com.kaspersky.android-components:kaspresso", versio test-kaspresso-compose = { module = "com.kaspersky.android-components:kaspresso-compose-support", version.ref = "kaspresso-compose"} test-compose-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose-junit" } test-hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" } +test-hilt = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } +test-hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } # endregion Test # region Other amplitude = { module = "com.amplitude:android-sdk", version.ref = "amplitude" } -appsflyer = { module = "com.appsflyer:af-android-sdk", version.ref = "appsflyer" } armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } @@ -222,6 +225,7 @@ lottie = { module = "com.airbnb.android:lottie", version.ref = "lottie" } material = { module = "com.google.android.material:material", version.ref = "googleMaterialComponent" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } +moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" } spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } @@ -250,5 +254,10 @@ camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" } web3j-core = { module = "org.web3j:core", version.ref = "web3j" } leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } +decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } +decompose-ext-compose = { module = "com.arkivanov.decompose:extensions-compose-jetpack", version.ref = "decompose" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } markdown = { module = "org.jetbrains:markdown", version.ref = "markdown" } # endregion Other diff --git a/jitpack.gradle b/jitpack.gradle deleted file mode 100644 index cd263447fd..0000000000 --- a/jitpack.gradle +++ /dev/null @@ -1,4 +0,0 @@ -ext.jitpackSdk = [ - group: 'com.github.Tangem', - version : '0.2.1', -] \ No newline at end of file diff --git a/libs/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..f9c7b8a359 --- /dev/null +++ b/libs/blockchain-sdk/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.libs.blockchain_sdk" +} + +dependencies { + + // region Core modules + implementation(projects.core.datasource) + implementation(projects.core.featuretoggles) + implementation(projects.core.utils) + // endregion + + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + + // region DI libraries + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + // endregion + + // region Other libraries + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + // endregion + + // region Firebase libraries + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.firebase.crashlytics) + // endregion + + // region Tangem libraries + implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } + implementation(deps.tangem.card.core) + // endregion +} \ 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..7adfd7b9e1 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -0,0 +1,105 @@ +package com.tangem.blockchainsdk + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.network.providers.ProviderType +import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter +import com.tangem.blockchainsdk.converters.BlockchainSDKConfigConverter +import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader +import com.tangem.blockchainsdk.store.RuntimeStore +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.config.models.ConfigValueModel +import com.tangem.datasource.config.models.ProviderModel +import com.tangem.libs.blockchain_sdk.BuildConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber + +internal typealias BlockchainProvidersResponse = Map> +internal typealias BlockchainProviderTypes = Map> + +/** + * Implementation of Blockchain SDK components factory + * + * @property assetLoader asset loader + * @property blockchainProvidersResponseLoader blockchain providers response loader + * @property configStore blockchain sdk config store + * @property blockchainProviderTypesStore blockchain provider types store + * @property walletManagerFactoryCreator wallet manager factory creator + * +[REDACTED_AUTHOR] + */ +internal class DefaultBlockchainSDKFactory( + private val assetLoader: AssetLoader, + private val blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + private val configStore: RuntimeStore, + private val blockchainProviderTypesStore: RuntimeStore, + private val walletManagerFactoryCreator: WalletManagerFactoryCreator, + dispatchers: CoroutineDispatcherProvider, +) : BlockchainSDKFactory { + + private val 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(), + // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] + transform = walletManagerFactoryCreator::create, + ) + .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) + } + + private fun CoroutineScope.updateBlockchainSDKConfig() { + launch { + val config = assetLoader.load(fileName = CONFIG_FILE_NAME) + + if (config == null) { + Timber.e("Error loading BlockchainSDKConfig") + return@launch + } + + Timber.d("Update BlockchainSDKConfig") + + configStore.store( + value = BlockchainSDKConfigConverter.convert(value = config), + ) + } + } + + private fun CoroutineScope.updateBlockchainProviderTypes() { + launch { + val response = blockchainProvidersResponseLoader.load() + + if (response == null) { + Timber.e("Error loading BlockchainProviderTypes") + return@launch + } + + Timber.d("Update BlockchainProviderTypes") + + blockchainProviderTypesStore.store( + value = BlockchainProviderTypesConverter.convert(response), + ) + } + } + + private companion object { + const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}" + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt new file mode 100644 index 0000000000..9ede47a471 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -0,0 +1,43 @@ +package com.tangem.blockchainsdk + +import com.tangem.blockchain.common.AccountCreator +import com.tangem.blockchain.common.BlockchainFeatureToggles +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.featuretoggles.BlockchainSDKFeatureToggles +import timber.log.Timber +import javax.inject.Inject + +/** + * Creator of [WalletManagerFactory] + * + * @property accountCreator account creator + * @property blockchainDataStorage blockchain data storage + * @property blockchainSDKLogger blockchain SDK logger + * +[REDACTED_AUTHOR] + */ +internal class WalletManagerFactoryCreator @Inject constructor( + private val accountCreator: AccountCreator, + private val blockchainDataStorage: BlockchainDataStorage, + private val blockchainSDKLogger: BlockchainSDKLogger, + private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, +) { + + fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { + Timber.d("Create WalletManagerFactory") + + return WalletManagerFactory( + config = config, + blockchainProviderTypes = blockchainProviderTypes, + accountCreator = accountCreator, + featureToggles = BlockchainFeatureToggles( + isCardanoTokenSupport = blockchainSDKFeatureToggles.isCardanoTokensSupportEnabled, + ), + blockchainDataStorage = blockchainDataStorage, + loggers = listOf(blockchainSDKLogger), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultAccountCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt similarity index 80% rename from core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultAccountCreator.kt rename to libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt index 1501279d83..be9daa7448 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultAccountCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/accountcreator/DefaultAccountCreator.kt @@ -1,14 +1,14 @@ -package com.tangem.datasource.local.blockchain +package com.tangem.blockchainsdk.accountcreator import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.toHexString +import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CreateUserNetworkAccountBody -import com.tangem.lib.auth.AuthProvider internal class DefaultAccountCreator( private val authProvider: AuthProvider, @@ -16,7 +16,10 @@ internal class DefaultAccountCreator( ) : AccountCreator { override suspend fun createAccount(blockchain: Blockchain, walletPublicKey: ByteArray): Result { - val request = CreateUserNetworkAccountBody(blockchain.id.removeSuffix("/test"), walletPublicKey.toHexString()) + val request = CreateUserNetworkAccountBody( + networkId = blockchain.id.removeSuffix("/test"), + walletPublicKey = walletPublicKey.toHexString(), + ) return try { val response = tangemTechApi.createUserNetworkAccount( cardPublicKey = authProvider.getCardPublicKey(), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt new file mode 100644 index 0000000000..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..7fd86c9aa1 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -0,0 +1,87 @@ +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, + polygonScanApiKey = value.polygonScanApiKey, + ) + } + + private fun createGetBlockCredentials(configValues: ConfigValueModel): GetBlockCredentials? { + return configValues.getBlockAccessTokens?.let { accessTokens -> + GetBlockCredentials( + xrp = GetBlockAccessToken(jsonRpc = accessTokens.xrp?.jsonRPC), + cardano = GetBlockAccessToken(rosetta = accessTokens.cardano?.rosetta), + avalanche = GetBlockAccessToken(jsonRpc = accessTokens.avalanche?.jsonRPC), + eth = GetBlockAccessToken(jsonRpc = accessTokens.eth?.jsonRPC), + etc = GetBlockAccessToken(jsonRpc = accessTokens.etc?.jsonRPC), + fantom = GetBlockAccessToken(jsonRpc = accessTokens.fantom?.jsonRPC), + rsk = GetBlockAccessToken(jsonRpc = accessTokens.rsk?.jsonRPC), + bsc = GetBlockAccessToken(jsonRpc = accessTokens.bsc?.jsonRPC), + polygon = GetBlockAccessToken(jsonRpc = accessTokens.polygon?.jsonRPC), + gnosis = GetBlockAccessToken(jsonRpc = accessTokens.gnosis?.jsonRPC), + cronos = GetBlockAccessToken(jsonRpc = accessTokens.cronos?.jsonRPC), + solana = GetBlockAccessToken(jsonRpc = accessTokens.solana?.jsonRPC), + ton = GetBlockAccessToken(jsonRpc = accessTokens.ton?.jsonRPC), + tron = GetBlockAccessToken(rest = accessTokens.tron?.rest), + cosmos = GetBlockAccessToken(rest = accessTokens.cosmos?.rest), + near = GetBlockAccessToken(jsonRpc = accessTokens.near?.jsonRPC), + aptos = GetBlockAccessToken(rest = accessTokens.aptos?.rest), + dogecoin = GetBlockAccessToken( + jsonRpc = accessTokens.dogecoin?.jsonRPC, + blockBookRest = accessTokens.dogecoin?.blockBookRest, + ), + litecoin = GetBlockAccessToken( + jsonRpc = accessTokens.litecoin?.jsonRPC, + blockBookRest = accessTokens.litecoin?.blockBookRest, + ), + dash = GetBlockAccessToken( + jsonRpc = accessTokens.dash?.jsonRPC, + blockBookRest = accessTokens.dash?.blockBookRest, + ), + bitcoin = GetBlockAccessToken( + jsonRpc = accessTokens.bitcoin?.jsonRPC, + blockBookRest = accessTokens.bitcoin?.blockBookRest, + ), + algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest), + zkSyncEra = GetBlockAccessToken(jsonRpc = accessTokens.zksync?.jsonRPC), + polygonZkEvm = GetBlockAccessToken(jsonRpc = accessTokens.polygonZkevm?.jsonRPC), + base = GetBlockAccessToken(jsonRpc = accessTokens.base?.jsonRPC), + ) + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultBlockchainDataStorage.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/datastorage/DefaultBlockchainDataStorage.kt similarity index 95% rename from core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultBlockchainDataStorage.kt rename to libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/datastorage/DefaultBlockchainDataStorage.kt index 3f3d7192f5..85a3bab59f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/blockchain/DefaultBlockchainDataStorage.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/datastorage/DefaultBlockchainDataStorage.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.blockchain +package com.tangem.blockchainsdk.datastorage import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt new file mode 100644 index 0000000000..7adb1648a3 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -0,0 +1,63 @@ +package com.tangem.blockchainsdk.di + +import com.tangem.blockchain.common.BlockchainSdkConfig +import com.tangem.blockchain.common.logging.BlockchainSDKLogger +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.blockchainsdk.DefaultBlockchainSDKFactory +import com.tangem.blockchainsdk.WalletManagerFactoryCreator +import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator +import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage +import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles +import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader +import com.tangem.blockchainsdk.store.DefaultRuntimeStore +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object BlockchainSDKFactoryModule { + + @Provides + @Singleton + fun provideBlockchainSDKFactory( + assetLoader: AssetLoader, + blockchainProvidersResponseLoader: BlockchainProvidersResponseLoader, + walletManagerFactoryCreator: WalletManagerFactoryCreator, + dispatchers: CoroutineDispatcherProvider, + ): BlockchainSDKFactory { + return DefaultBlockchainSDKFactory( + assetLoader = assetLoader, + blockchainProvidersResponseLoader = blockchainProvidersResponseLoader, + configStore = DefaultRuntimeStore(defaultValue = BlockchainSdkConfig()), + blockchainProviderTypesStore = DefaultRuntimeStore(defaultValue = emptyMap()), + walletManagerFactoryCreator = walletManagerFactoryCreator, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideWalletManagerFactoryCreator( + authProvider: AuthProvider, + tangemTechApi: TangemTechApi, + appPreferencesStore: AppPreferencesStore, + blockchainSDKLogger: BlockchainSDKLogger, + featureTogglesManager: FeatureTogglesManager, + ): WalletManagerFactoryCreator { + return WalletManagerFactoryCreator( + accountCreator = DefaultAccountCreator(authProvider, tangemTechApi), + blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), + blockchainSDKLogger = blockchainSDKLogger, + blockchainSDKFeatureToggles = DefaultBlockchainSDKFeatureToggles(featureTogglesManager), + ) + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt new file mode 100644 index 0000000000..fcdc2099b3 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.blockchainsdk.featuretoggles + +internal interface BlockchainSDKFeatureToggles { + + val isCardanoTokensSupportEnabled: Boolean +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt new file mode 100644 index 0000000000..ce7241d7d5 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.blockchainsdk.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultBlockchainSDKFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : BlockchainSDKFeatureToggles { + + override val isCardanoTokensSupportEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "CARDANO_TOKENS_SUPPORT_ENABLED") +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt new file mode 100644 index 0000000000..310ec39f9a --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/loader/BlockchainProvidersResponseLoader.kt @@ -0,0 +1,97 @@ +package com.tangem.blockchainsdk.loader + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.api.tangemTech.TangemTechServiceApi +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Loader of [BlockchainProvidersResponse] + * + * @property tangemTechServiceApi tangem tech api + * @property authProvider auth provider + * @property assetLoader asset loader for local config loading + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class BlockchainProvidersResponseLoader @Inject constructor( + private val tangemTechServiceApi: TangemTechServiceApi, + private val authProvider: AuthProvider, + private val assetLoader: AssetLoader, + private val dispatchers: CoroutineDispatcherProvider, +) { + + private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance) + + /** Load [BlockchainProvidersResponse] */ + suspend fun load(): BlockchainProvidersResponse? { + val localResponse = loadLocal() ?: return null + + return runCatching(dispatcher = dispatchers.io, block = ::loadRemote) + .fold( + onSuccess = { remoteResponse -> mergeResponses(local = localResponse, remote = remoteResponse) }, + onFailure = { + Timber.e(it, "Failed to load blockchain provider types from backend") + localResponse + }, + ) + } + + private suspend fun loadLocal(): BlockchainProvidersResponse? { + return assetLoader.load(fileName = PROVIDER_TYPES_FILE_NAME) + } + + private suspend fun loadRemote(): BlockchainProvidersResponse { + return tangemTechServiceApi.getBlockchainProviders( + cardPublicKey = authProvider.getCardPublicKey(), + cardId = authProvider.getCardId(), + ) + } + + /** Merge blockchains with non-empty providers [remote] from remote with blockchains from local [local] */ + private fun mergeResponses( + local: BlockchainProvidersResponse, + remote: BlockchainProvidersResponse, + ): BlockchainProvidersResponse { + /* + * Example: + * val remote = mapOf("a" to 1, "b" to 2, "c" to 3) + * val local = mapOf("a" to 11, "e" to 4, "f" to 5) + * + * local + remote // { a = 1, e = 4, f = 5, b = 2, c = 3 } + */ + val result = local + remote.filterValues { it.isNotEmpty() } + + if (result != remote) { + val missingBlockchains = result.keys - remote.keys + val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys + + recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) + } + + return result + } + + private fun recordException(missingBlockchains: Set) { + val exception = IllegalStateException( + "Remote config does not contain required blockchains or providers information: " + + missingBlockchains.joinToString(), + ) + + Timber.e(exception) + + firebaseCrashlytics.recordException(exception) + } + + private companion object { + const val PROVIDER_TYPES_FILE_NAME = "tangem-app-config/providers_order" + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt new file mode 100644 index 0000000000..4278918e98 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/DefaultRuntimeStore.kt @@ -0,0 +1,20 @@ +package com.tangem.blockchainsdk.store + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Default implementation of RuntimeStore + * + * @param defaultValue default value + */ +internal class DefaultRuntimeStore(defaultValue: T) : RuntimeStore { + + private val flow = MutableStateFlow(value = defaultValue) + + override fun get(): StateFlow = flow + + override suspend fun store(value: T) { + flow.value = value + } +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt new file mode 100644 index 0000000000..2969e42576 --- /dev/null +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/store/RuntimeStore.kt @@ -0,0 +1,17 @@ +package com.tangem.blockchainsdk.store + +import kotlinx.coroutines.flow.StateFlow + +/** + * Runtime store + * +[REDACTED_AUTHOR] + */ +internal interface RuntimeStore { + + /** Get flow of elements [T] */ + fun get(): StateFlow + + /** Store [value] */ + suspend fun store(value: T) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt similarity index 97% rename from domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt rename to libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index dc608f1ae4..00ee28bafb 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,9 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "taraxa/test" -> Blockchain.TaraxaTestnet "base" -> Blockchain.Base "base/test" -> Blockchain.BaseTestnet + "koinos" -> Blockchain.Koinos + "koinos/test" -> Blockchain.KoinosTestnet + "joystream" -> Blockchain.Joystream else -> null } } @@ -229,6 +232,9 @@ fun Blockchain.toNetworkId(): String { Blockchain.TaraxaTestnet -> "taraxa/test" Blockchain.Base -> "base" Blockchain.BaseTestnet -> "base/test" + Blockchain.Koinos -> "koinos" + Blockchain.KoinosTestnet -> "koinos/test" + Blockchain.Joystream -> "joystream" } } @@ -302,6 +308,8 @@ fun Blockchain.toCoinId(): String { Blockchain.Flare, Blockchain.FlareTestnet -> "flare-networks" Blockchain.Taraxa, Blockchain.TaraxaTestnet -> "taraxa" Blockchain.Base, Blockchain.BaseTestnet -> "base-ethereum" + Blockchain.Koinos, Blockchain.KoinosTestnet -> "koinos" + Blockchain.Joystream -> "joystream" } } @@ -330,9 +338,10 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, - Blockchain.Radiant, Blockchain.Manta, Blockchain.MantaTestnet, Blockchain.Mantle, Blockchain.MantleTestnet, + Blockchain.Koinos, + Blockchain.KoinosTestnet, ) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 85ef064f98..35b8386cd9 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -45,6 +45,11 @@ object BlockchainUtils { return blockchain == Blockchain.Tezos } + fun isCardano(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Cardano + } + /** If current [networkId] is BeaconChain */ fun isBeaconChain(networkId: String): Boolean { val blockchain = Blockchain.fromId(networkId) diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index c2e37da872..9bdde82e08 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -1,6 +1,5 @@ package com.tangem.lib.crypto -import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.ProxyAmount /** @@ -8,28 +7,11 @@ import com.tangem.lib.crypto.models.ProxyAmount */ interface UserWalletManager { - /** - * Returns all user tokens (merged from local and backend) - */ - @Throws(IllegalStateException::class) - suspend fun getUserTokens(networkId: String, derivationPath: String?, isExcludeCustom: Boolean): List - - @Throws(IllegalStateException::class) - fun getNativeTokenForNetwork(networkId: String): Currency - /** * Returns user walletId or empty string */ fun getWalletId(): String - /** - * Checks that token added to user wallet - * - * @param currency to receive referral payments - */ - @Throws(IllegalStateException::class) - suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean - suspend fun hideAllTokens() /** @@ -41,21 +23,6 @@ interface UserWalletManager { @Throws(IllegalStateException::class) suspend fun getWalletAddress(networkId: String, derivationPath: String?): String - /** - * Return balances from wallet found by networkId - * - * @param networkId - * @param extraTokens tokens you want to check balance that not exists in wallet - * @param derivationPath if null uses default - * @return map of - */ - @Throws(IllegalStateException::class) - suspend fun getCurrentWalletTokensBalance( - networkId: String, - extraTokens: List, - derivationPath: String?, - ): Map - @Throws(IllegalStateException::class) suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt index 56b92d5688..a4643460ea 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt @@ -1,8 +1,21 @@ package com.tangem.lib.crypto.models +import java.math.BigDecimal import java.math.BigInteger -data class ProxyFee( - val gasLimit: BigInteger, - val fee: ProxyAmount, -) \ No newline at end of file +sealed interface ProxyFee { + + val gasLimit: BigInteger + val fee: ProxyAmount + + data class Common( + override val gasLimit: BigInteger, + override val fee: ProxyAmount, + ) : ProxyFee + + data class CardanoToken( + override val gasLimit: BigInteger, + override val fee: ProxyAmount, + val minAdaValue: BigDecimal, + ) : ProxyFee +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index f288d89a7d..885f32c1d7 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -37,7 +37,7 @@ private fun AppExtension.configureDefaultConfig(project: Project) { buildFeatures.buildConfig = true - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner = "com.tangem.common.HiltTestRunner" } } @@ -76,6 +76,7 @@ private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buil } BuildType.Internal, BuildType.External, + BuildType.Mocked -> { initWith(extension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt index 66437a1de5..17f61ecdd2 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt @@ -15,6 +15,7 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v value = "\"$value\"", ) + // TODO remove class TestActionEnabled(isEnabled: Boolean) : BuildConfigField( type = "Boolean", name = "TEST_ACTION_ENABLED", @@ -32,4 +33,10 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v name = "TESTER_MENU_ENABLED", value = isEnabled.toString(), ) + + class MockDataSource(isEnabled: Boolean) : BuildConfigField( + type = "Boolean", + name = "MOCK_DATA_SOURCE", + value = isEnabled.toString(), + ) } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 767e46ba25..53a1c18637 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -27,6 +27,29 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), + BuildConfigField.MockDataSource(isEnabled = false), + ), + ), + + /** + * Build type for QA and business + * + * Features: + * - Env: dev + * - Signing config: debug + * - Logs + * - Enabled mocked datasource + * */ + Mocked( + id = "mocked", + appIdSuffix = "mocked", + versionSuffix = "mocked", + configFields = listOf( + BuildConfigField.Environment(value = "dev"), + BuildConfigField.TestActionEnabled(isEnabled = false), + BuildConfigField.LogEnabled(isEnabled = true), + BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = true), ), ), @@ -50,6 +73,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), + BuildConfigField.MockDataSource(isEnabled = false), ), ), @@ -70,6 +94,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = false), ), ), @@ -88,6 +113,7 @@ internal enum class BuildType( BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.MockDataSource(isEnabled = false), ), ), } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8c94ba65a8..eeeaa4c19a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,11 +24,41 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { - google() + google { + content { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() - mavenLocal() - jcenter() // unable to replace with mavenCentral() due to rekotlin - maven("https://nexus.tangem-tech.com/repository/maven-releases/") + mavenLocal { + content { + includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") + includeModule("com.tangem", "blstlib") + includeModule("com.tangem", "blockchain") + includeModule("com.tangem", "wallet-core-proto") + includeModule("com.tangem", "wallet-core") + } + } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/tangem-sdk-android") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") } + } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/blst-android") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { includeModule("com.tangem", "blstlib") } + } maven { // setting any repository from tangem project allows maven search all packages in the project url = uri("https://maven.pkg.github.com/tangem/blockchain-sdk-kotlin") @@ -36,6 +66,7 @@ dependencyResolutionManagement { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") } + content { includeModule("com.tangem", "blockchain") } } maven { // setting any repository from tangem project allows maven search all packages in the project @@ -44,6 +75,15 @@ dependencyResolutionManagement { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") } + content { + includeModule("com.tangem", "wallet-core-proto") + includeModule("com.tangem", "wallet-core") + } + } + jcenter { // unable to replace with mavenCentral() due to rekotlin + content { + includeModule("org.rekotlin", "rekotlin") + } } maven("https://jitpack.io") maven("https://clients-nexus.sprinklr.com/") @@ -72,11 +112,13 @@ include(":core:ui") include(":core:utils") include(":core:deep-links") include(":core:deep-links:global") +include(":core:decompose") // endregion Core modules // region Libs modules -include(":libs:crypto") include(":libs:auth") +include(":libs:blockchain-sdk") +include(":libs:crypto") include(":libs:visa") // endregion Libs modules @@ -111,6 +153,9 @@ include(":features:manage-tokens:impl") include(":features:qr-scanning:api") include(":features:qr-scanning:impl") + +include(":features:staking:api") +include(":features:staking:impl") // endregion Feature modules // region Domain modules @@ -135,6 +180,7 @@ include(":domain:app-theme:models") include(":domain:balance-hiding") include(":domain:balance-hiding:models") include(":domain:transaction") +include(":domain:transaction:models") include(":domain:analytics") include(":domain:visa") include(":domain:onboarding") @@ -150,7 +196,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")