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 4096521b75..b153b0e0de 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 @@ -6,6 +6,8 @@ import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -28,6 +30,17 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { + @Provides + fun providesUserWalletsSyncDelegate( + userWalletsListManager: UserWalletsListManager, + dispatchers: CoroutineDispatcherProvider, + ): UserWalletsSyncDelegate { + return DefaultUserWalletsSyncDelegate( + userWalletsListManager = userWalletsListManager, + dispatchers = dispatchers, + ) + } + @Provides @Singleton fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { @@ -102,10 +115,13 @@ internal object WalletsDomainModule { @Provides @Singleton fun providesRenameWalletUseCase( - userWalletsListManager: UserWalletsListManager, - dispatchers: CoroutineDispatcherProvider, + walletsRepository: WalletsRepository, + userWalletsSyncDelegate: UserWalletsSyncDelegate, ): RenameWalletUseCase { - return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers) + return RenameWalletUseCase( + walletsRepository = walletsRepository, + userWalletsSyncDelegate = userWalletsSyncDelegate, + ) } @Provides @@ -197,4 +213,16 @@ internal object WalletsDomainModule { nftFeatureToggles = nftFeatureToggles, ) } + + @Provides + @Singleton + fun providesUpdateRemoteWalletsInfoUseCase( + walletsRepository: WalletsRepository, + userWalletsSyncDelegate: UserWalletsSyncDelegate, + ): UpdateRemoteWalletsInfoUseCase { + return UpdateRemoteWalletsInfoUseCase( + walletsRepository = walletsRepository, + userWalletsSyncDelegate = userWalletsSyncDelegate, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 689db8c1c0..0ac30a5cb0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -13,6 +13,7 @@ import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper @@ -64,14 +65,18 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor( if (isNewWc) stub.updateSessions() else legacy.updateSessions() } - override fun pair(uri: String, source: SourceType) { + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { val src = when (source) { SourceType.QR -> WcPairRequest.Source.QR SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD SourceType.ETC -> WcPairRequest.Source.ETC } - if (isNewWc) wcPairService.pair(WcPairRequest(uri, src)) else legacy.pair(uri, source) + if (isNewWc) { + wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId)) + } else { + legacy.pair(userWalletId = userWalletId, uri = uri, source = source) + } } override fun disconnect(topic: String) { @@ -110,7 +115,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository override fun updateSessions() = Unit - override fun pair(uri: String, source: SourceType) = Unit + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit override fun disconnect(topic: String) = Unit @@ -405,7 +410,7 @@ internal class DefaultLegacyWalletConnectRepository( this.userNamespaces = userNamespaces } - override fun pair(uri: String, source: SourceType) { + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source)) WalletKit.pair( params = Wallet.Params.Pair(uri), diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 59d5954e00..c0e22a3b7d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.domain.walletconnect.model.legacy.Account +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType import kotlinx.coroutines.flow.Flow @@ -19,7 +20,7 @@ interface LegacyWalletConnectRepository { fun updateSessions() - fun pair(uri: String, source: SourceType) + fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) fun disconnect(topic: String) 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 d8d5a989ae..92418004d0 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 @@ -12,6 +12,7 @@ import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsReposit 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.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.filterNotNull @@ -139,7 +140,11 @@ class WalletConnectInteractor( if (deeplinkStack.empty()) return val lastDeeplink = deeplinkStack.pop() val action = WalletConnectAction - .OpenSession(lastDeeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK) + .OpenSession( + wcUri = lastDeeplink, + source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, + userWalletId = UserWalletId(userWalletId), + ) store.dispatchOnMain(action) }.onFailure { Timber.e("WC deeplink handling failed. $it") @@ -393,7 +398,11 @@ class WalletConnectInteractor( } if (isWalletConnectReadyForDeepLinks) { - val action = WalletConnectAction.OpenSession(deeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK) + val action = WalletConnectAction.OpenSession( + wcUri = deeplink, + source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, + userWalletId = UserWalletId(userWalletId), + ) store.dispatchOnMain(action) } else { deeplinkStack.push(deeplink) 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 7391c77f5b..4288f711b9 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,5 +1,6 @@ package com.tangem.tap.features.details.redux.walletconnect +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents @@ -14,6 +15,7 @@ sealed class WalletConnectAction : Action { data class OpenSession( val wcUri: String, val source: SourceType, + val userWalletId: UserWalletId, ) : WalletConnectAction() { enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC } } 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 6bbd548de4..7439241a50 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 @@ -63,7 +63,11 @@ class WalletConnectMiddleware { is WalletConnectAction.OpenSession -> { val index = action.wcUri.indexOf("@") when (action.wcUri[index + 1]) { - '2' -> walletConnectRepository.pair(uri = action.wcUri, source = action.source) + '2' -> walletConnectRepository.pair( + uri = action.wcUri, + source = action.source, + userWalletId = action.userWalletId, + ) '1' -> { store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest) store.dispatchOnMain( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt index 73a0989519..0974e0db1b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt @@ -22,10 +22,10 @@ import org.rekotlin.StoreSubscriber @Suppress("UnusedPrivateMember") internal class DefaultWalletConnectComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Assisted params: WalletConnectComponent.Params, ) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber { - private val model: WalletConnectModel = getOrCreateModel() + private val model: WalletConnectModel = getOrCreateModel(params) private var screenState: MutableState = mutableStateOf(model.updateState(store.state.walletConnectState)) @@ -65,6 +65,9 @@ internal class DefaultWalletConnectComponent @AssistedInject constructor( @AssistedFactory interface Factory : WalletConnectComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectComponent + override fun create( + context: AppComponentContext, + params: WalletConnectComponent.Params, + ): DefaultWalletConnectComponent } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt index 21deae59a7..762b7afb71 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt @@ -4,10 +4,12 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList @@ -22,13 +24,22 @@ import javax.inject.Inject internal class WalletConnectModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + paramsContainer: ParamsContainer, ) : Model() { + private val params = paramsContainer.require() + init { modelScope.launch { listenToQrScanningUseCase(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .map { WalletConnectAction.OpenSession(it, WalletConnectAction.OpenSession.SourceType.QR) } + .map { + WalletConnectAction.OpenSession( + wcUri = it, + source = WalletConnectAction.OpenSession.SourceType.QR, + userWalletId = params.userWalletId, + ) + } .collect { store.dispatch(it) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt index 4e1f4ab3fb..6b9d8c4330 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt @@ -2,7 +2,9 @@ package com.tangem.tap.features.details.ui.walletconnect.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface WalletConnectComponent : ComposableContentComponent { - interface Factory : ComponentFactory + data class Params(val userWalletId: UserWalletId) + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 0f2b7d5303..f1cd71bbe3 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -291,13 +291,13 @@ internal class ChildFactory @Inject constructor( if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { createComponentChild( context = context, - params = Unit, + params = RedesignedWalletConnectComponent.Params(route.userWalletId), componentFactory = redesignedWalletConnectComponentFactory, ) } else { createComponentChild( context = context, - params = Unit, + params = WalletConnectComponent.Params(route.userWalletId), componentFactory = walletConnectComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 0a280474be..af27366ceb 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -122,7 +122,7 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions") + data class WalletConnectSessions(val userWalletId: UserWalletId) : AppRoute(path = "/wallet_connect_sessions") @Serializable data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") { 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 7b474dd442..d4db8f1fa1 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 @@ -172,6 +172,9 @@ interface TangemTechApi { @GET("user-wallets/wallets/{wallet_id}") suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse + + @GET("user-wallets/wallets/by-app/{app_id}") + suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion companion object { diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 1470589ef2..62f837994d 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -4,12 +4,12 @@ import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConv import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody -import com.tangem.datasource.api.tangemTech.models.WalletBody import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.* +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -23,7 +23,7 @@ internal class DefaultNotificationsRepository @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : NotificationsRepository { - override suspend fun createApplicationId(pushToken: String?): String = withContext(dispatchers.io) { + override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { tangemTechApi.createApplicationId( NotificationApplicationCreateBody( platform = appInfoProvider.platform, @@ -33,15 +33,16 @@ internal class DefaultNotificationsRepository @Inject constructor( timezone = appInfoProvider.timezone, pushToken = pushToken, ), - ).getOrThrow().appId + ).getOrThrow().appId.let(::ApplicationId) } - override suspend fun saveApplicationId(appId: String) { - appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId) + override suspend fun saveApplicationId(appId: ApplicationId) { + appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) } - override suspend fun getApplicationId(): String? { + override suspend fun getApplicationId(): ApplicationId? { return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) + ?.let(::ApplicationId) } override suspend fun incrementTronTokenFeeNotificationShowCounter() { @@ -71,21 +72,10 @@ internal class DefaultNotificationsRepository @Inject constructor( ).getOrThrow() } - override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { - tangemTechApi.updateWallet( - walletId, - WalletBody(name = walletName), - ).getOrThrow() - } - - override suspend fun getWalletName(walletId: String): String? = withContext(dispatchers.io) { - tangemTechApi.getWalletById(walletId).getOrThrow().name - } - - override suspend fun sendPushToken(appId: String, pushToken: String) { + override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { withContext(dispatchers.io) { tangemTechApi.updatePushTokenForApplicationId( - appId, + appId.value, NotificationApplicationCreateBody( pushToken = pushToken, ), diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 305732cde6..57d8d4c429 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -12,6 +12,7 @@ import com.squareup.moshi.Moshi import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -41,9 +42,9 @@ class DefaultNotificationsRepositoryTest { fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { // GIVEN val pushToken = "test-push-token" - val expectedAppId = "test-app-id" + val expectedAppId = ApplicationId("test-app-id") val expectedAppIdResponse = NotificationApplicationIdResponse( - appId = expectedAppId, + appId = expectedAppId.value, ) coEvery { appInfoProvider.platform } returns "android" coEvery { appInfoProvider.device } returns "test-device" @@ -76,7 +77,7 @@ class DefaultNotificationsRepositoryTest { @Test fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { // GIVEN - val appId = "test-app-id" + val appId = ApplicationId("test-app-id") val preferences = mockk(relaxed = true) coEvery { preferencesDataStore.updateData(any()) } returns preferences @@ -90,10 +91,10 @@ class DefaultNotificationsRepositoryTest { @Test fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { // GIVEN - val expectedAppId = "test-app-id" + val expectedAppId = ApplicationId("test-app-id") val preferences = mockk(relaxed = true) val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) - every { preferences[key] } returns expectedAppId + every { preferences[key] } returns expectedAppId.value coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN @@ -122,56 +123,24 @@ class DefaultNotificationsRepositoryTest { coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) } } - @Test - fun `GIVEN wallet id and name WHEN setWalletName THEN updates wallet name`() = runTest { - // GIVEN - val walletId = "test-wallet-id" - val walletName = "Test Wallet" - coEvery { - tangemTechApi.updateWallet( - walletId, - WalletBody(name = walletName), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.setWalletName(walletId, walletName) - - // THEN - coVerify { tangemTechApi.updateWallet(walletId, WalletBody(name = walletName)) } - } - - @Test - fun `GIVEN wallet id WHEN getWalletName THEN returns wallet name`() = runTest { - // GIVEN - val walletId = "test-wallet-id" - val expectedName = "Test Wallet" - coEvery { tangemTechApi.getWalletById(walletId) } returns ApiResponse.Success( - WalletResponse( - notifyStatus = false, - name = expectedName, - id = walletId, - ), - ) - - // WHEN - val result = repository.getWalletName(walletId) - - // THEN - assertThat(result).isEqualTo(expectedName) - } - @Test fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { // GIVEN - val appId = "test-app-id" + val appId = ApplicationId("test-app-id") val pushToken = "test-push-token" coEvery { tangemTechApi.updatePushTokenForApplicationId( - appId, - NotificationApplicationCreateBody(pushToken = pushToken), + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + platform = null, + device = null, + systemVersion = null, + language = null, + timezone = null, + ), ) - } returns ApiResponse.Success(appId) + } returns ApiResponse.Success(appId.value) // WHEN repository.sendPushToken(appId, pushToken) @@ -179,8 +148,15 @@ class DefaultNotificationsRepositoryTest { // THEN coVerify { tangemTechApi.updatePushTokenForApplicationId( - appId, - NotificationApplicationCreateBody(pushToken = pushToken), + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + platform = null, + device = null, + systemVersion = null, + language = null, + timezone = null, + ), ) } } diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 1792340585..9b0b650724 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -20,6 +20,7 @@ import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.pair.WcPairState +import com.tangem.domain.wallets.models.UserWalletId import io.mockk.coEvery import io.mockk.coVerifyOrder import io.mockk.mockk @@ -99,7 +100,7 @@ internal class DefaultWcPairUseCaseTest { caipNamespaceDelegate = caipNamespaceDelegate, sdkDelegate = sdkDelegate, blockAidVerifier = blockAidVerifier, - pairRequest = WcPairRequest(url, source), + pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source), ) @Before diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 39096c2551..25bc5ae0b6 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.wallets +import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -18,6 +19,7 @@ import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -28,6 +30,7 @@ import kotlinx.coroutines.withContext typealias SeedPhraseNotificationsStatuses = Map +@Suppress("TooManyFunctions") internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, @@ -254,6 +257,37 @@ internal class DefaultWalletsRepository( } } + override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { + tangemTechApi.updateWallet( + walletId = walletId, + body = WalletBody(name = walletName), + ).getOrThrow() + } + + override suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo = withContext(dispatchers.io) { + UserWalletRemoteInfoConverter.convert( + value = tangemTechApi.getWalletById(walletId).getOrThrow(), + ) + } + + override suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean): List = + withContext(dispatchers.io) { + tangemTechApi.getWallets(applicationId) + .getOrThrow() + .map { walletInfo -> + val userWallet = UserWalletRemoteInfoConverter.convert( + value = walletInfo, + ) + if (updateCache) { + setNotificationsEnabledLocally( + userWalletId = userWallet.walletId, + isEnabled = userWallet.isNotificationsEnabled, + ) + } + userWallet + } + } + private suspend fun loadAndSaveNotificationsEnabled(userWalletId: UserWalletId): Boolean { val walletResponse = tangemTechApi.getWalletById(walletId = userWalletId.stringValue).getOrThrow() val isEnabled = walletResponse.notifyStatus diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/converters/UserWalletRemoteInfoConverter.kt b/data/wallets/src/main/java/com/tangem/data/wallets/converters/UserWalletRemoteInfoConverter.kt new file mode 100644 index 0000000000..4ac595663e --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/converters/UserWalletRemoteInfoConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.wallets.converters + +import com.tangem.datasource.api.tangemTech.models.WalletResponse +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.utils.converter.Converter + +internal object UserWalletRemoteInfoConverter : Converter { + override fun convert(value: WalletResponse): UserWalletRemoteInfo { + return UserWalletRemoteInfo( + walletId = UserWalletId(value.id), + name = value.name.orEmpty(), + isNotificationsEnabled = value.notifyStatus, + ) + } +} \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 28f07454c2..f430dab62f 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -173,4 +173,62 @@ class DefaultWalletsRepositoryTest { } coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } } + + @Test + fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() = runTest { + // GIVEN + val applicationId = "test_app_id" + val wallet1Id = "1234567890abcdef" + val wallet2Id = "fedcba0987654321" + val walletResponses = listOf( + WalletResponse( + id = wallet1Id, + notifyStatus = true, + ), + WalletResponse( + id = wallet2Id, + notifyStatus = false, + ), + ) + coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + val result = repository.getWalletsInfo(applicationId, updateCache = true) + + // THEN + assertThat(result).hasSize(2) + assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) + assertThat(result[0].isNotificationsEnabled).isTrue() + assertThat(result[1].walletId.stringValue).isEqualTo(wallet2Id) + assertThat(result[1].isNotificationsEnabled).isFalse() + + coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) } + coVerify(exactly = 2) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() = runTest { + // GIVEN + val applicationId = "test_app_id" + val wallet1Id = "1234567890abcdef" + val walletResponses = listOf( + WalletResponse( + id = wallet1Id, + notifyStatus = true, + ), + ) + coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses) + + // WHEN + val result = repository.getWalletsInfo(applicationId, updateCache = false) + + // THEN + assertThat(result).hasSize(1) + assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) + assertThat(result[0].isNotificationsEnabled).isTrue() + + coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) } + coVerify(exactly = 0) { preferencesDataStore.updateData(any()) } + } } \ No newline at end of file diff --git a/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/ApplicationId.kt b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/ApplicationId.kt new file mode 100644 index 0000000000..62db4696b3 --- /dev/null +++ b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/ApplicationId.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.notifications.models + +@JvmInline +value class ApplicationId(val value: String) \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt index 026d3a89c6..963d3b99d7 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.notifications import arrow.core.Either +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -10,7 +11,7 @@ class GetApplicationIdUseCase( ) { private val mutex = Mutex() - suspend operator fun invoke(): Either = Either.catch { + suspend operator fun invoke(): Either = Either.catch { val localApplicationId = notificationsRepository.getApplicationId() if (localApplicationId != null) return@catch localApplicationId diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index b7f53ab51e..d5e99ab745 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -1,15 +1,16 @@ package com.tangem.domain.notifications.repository +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.models.NotificationsEligibleNetwork interface NotificationsRepository { @Throws - suspend fun createApplicationId(pushToken: String? = null): String + suspend fun createApplicationId(pushToken: String? = null): ApplicationId - suspend fun saveApplicationId(appId: String) + suspend fun saveApplicationId(appId: ApplicationId) - suspend fun getApplicationId(): String? + suspend fun getApplicationId(): ApplicationId? suspend fun getTronTokenFeeNotificationShowCounter(): Int @@ -19,13 +20,7 @@ interface NotificationsRepository { suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) @Throws - suspend fun setWalletName(walletId: String, walletName: String) - - @Throws - suspend fun getWalletName(walletId: String): String? - - @Throws - suspend fun sendPushToken(appId: String, pushToken: String) + suspend fun sendPushToken(appId: ApplicationId, pushToken: String) @Throws suspend fun getEligibleNetworks(): List diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt index 350f5bad73..6b2984554b 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -2,6 +2,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import io.mockk.coEvery import io.mockk.coVerify @@ -20,7 +21,7 @@ class GetApplicationIdUseCaseTest { @Test fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest { // GIVEN - val expectedApplicationId = "test-app-id" + val expectedApplicationId = ApplicationId("test-app-id") coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId // WHEN @@ -39,7 +40,7 @@ class GetApplicationIdUseCaseTest { @Test fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest { // GIVEN - val newApplicationId = "new-app-id" + val newApplicationId = ApplicationId("new-app-id") coEvery { notificationsRepository.getApplicationId() } returns null coEvery { notificationsRepository.createApplicationId() } returns newApplicationId coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit @@ -81,7 +82,7 @@ class GetApplicationIdUseCaseTest { fun `GIVEN no local application ID WHEN multiple concurrent invokes THEN create only one application ID`() = runTest { // GIVEN - val newApplicationId = "new-app-id" + val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false coEvery { notificationsRepository.getApplicationId() } answers { @@ -115,7 +116,7 @@ class GetApplicationIdUseCaseTest { @Test fun `GIVEN no local application ID WHEN multiple concurrent invokes with delay THEN create only one application ID`() = runTest { // GIVEN - val newApplicationId = "new-app-id" + val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false coEvery { notificationsRepository.getApplicationId() } answers { diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt index aeb18310a1..f079a021d1 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt @@ -2,6 +2,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat +import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider import io.mockk.coEvery @@ -33,7 +34,7 @@ class SendPushTokenUseCaseTest { @Test fun `GIVEN valid application ID and token WHEN invoke THEN token is sent successfully`() = runTest { // GIVEN - val applicationId = "test-app-id" + val applicationId = ApplicationId("test-app-id") val token = "test-token" coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) coEvery { pushNotificationsTokenProvider.getToken() } returns token @@ -62,7 +63,7 @@ class SendPushTokenUseCaseTest { @Test fun `GIVEN repository throws error WHEN invoke THEN returns error`() = runTest { // GIVEN - val applicationId = "test-app-id" + val applicationId = ApplicationId("test-app-id") val token = "test-token" val expectedError = RuntimeException("Network error") coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt index 1c1c3f3674..2d70af0e55 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt @@ -1,11 +1,13 @@ package com.tangem.domain.walletconnect.model +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @Serializable data class WcPairRequest( val uri: String, val source: Source, + val userWalletId: UserWalletId, ) { enum class Source { QR, DEEPLINK, CLIPBOARD, ETC } } \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 6ca312b904..331335a257 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.notifications.models) // endregion // region Tangem libraries @@ -37,4 +38,11 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) // end + + // region Tests + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) + // end } \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt new file mode 100644 index 0000000000..5c6e3669a5 --- /dev/null +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletRemoteInfo.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.wallets.models + +class UserWalletRemoteInfo( + val walletId: UserWalletId, + val name: String, + val isNotificationsEnabled: Boolean, +) \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt new file mode 100644 index 0000000000..b1c1e11c7e --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -0,0 +1,55 @@ +package com.tangem.domain.wallets.delegate + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.common.CompletionResult +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 +import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class DefaultUserWalletsSyncDelegate( + private val userWalletsListManager: UserWalletsListManager, + private val dispatchers: CoroutineDispatcherProvider, +) : UserWalletsSyncDelegate { + + override suspend fun syncWallet(userWalletId: UserWalletId, name: String): Either { + return renameUserWallet(userWalletId, name) + } + + override suspend fun syncWallets(list: List): Either = either { + list.forEach { userWallet -> + renameUserWallet(userWallet.walletId, userWallet.name).bind() + } + } + + private suspend fun renameUserWallet( + userWalletId: UserWalletId, + name: String, + ): Either = withContext(dispatchers.io) { + either { + val existingNames = userWalletsListManager.userWalletsSync + + ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } + + val previousName = existingNames.firstOrNull { it.walletId == userWalletId }?.name.orEmpty() + if (previousName == name) { + raise(UpdateWalletError.NameAlreadyExists) + } + + return@withContext when ( + val result = + userWalletsListManager.update(userWalletId) { it.copy(name = name) } + ) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } + } + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/UserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/UserWalletsSyncDelegate.kt new file mode 100644 index 0000000000..94f72df038 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/UserWalletsSyncDelegate.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.wallets.delegate + +import arrow.core.Either +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo + +interface UserWalletsSyncDelegate { + + suspend fun syncWallet(userWalletId: UserWalletId, name: String): Either + + suspend fun syncWallets(list: List): Either +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 541e601c54..90768f5e78 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -2,8 +2,10 @@ package com.tangem.domain.wallets.repository import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWalletRemoteInfo import kotlinx.coroutines.flow.Flow +@Suppress("TooManyFunctions") interface WalletsRepository { suspend fun shouldSaveUserWalletsSync(): Boolean @@ -43,4 +45,13 @@ interface WalletsRepository { @Throws suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) + + @Throws + suspend fun setWalletName(walletId: String, walletName: String) + + @Throws + suspend fun getWalletInfo(walletId: String): UserWalletRemoteInfo + + @Throws + suspend fun getWalletsInfo(applicationId: String, updateCache: Boolean = true): List } \ No newline at end of file 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 index ff568c6794..8225cd9220 100644 --- 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 @@ -2,38 +2,23 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.common.CompletionResult -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext +import com.tangem.domain.wallets.repository.WalletsRepository -/** - * Use case for rename user wallet - * - * @property userWalletsListManager user wallets list manager - */ class RenameWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, - private val dispatchers: CoroutineDispatcherProvider, + private val walletsRepository: WalletsRepository, + private val userWalletsSyncDelegate: UserWalletsSyncDelegate, ) { suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either = - withContext(dispatchers.io) { - either { - val existingNames = userWalletsListManager.userWalletsSync - - ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { - UpdateWalletError.NameAlreadyExists - } - - when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data - } + either { + runCatching { + walletsRepository.setWalletName(userWalletId.stringValue, name) } + + userWalletsSyncDelegate.syncWallet(userWalletId, name).bind() } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt new file mode 100644 index 0000000000..5305089cd7 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateRemoteWalletsInfoUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.repository.WalletsRepository + +class UpdateRemoteWalletsInfoUseCase( + private val walletsRepository: WalletsRepository, + private val userWalletsSyncDelegate: UserWalletsSyncDelegate, +) { + + suspend operator fun invoke(applicationId: ApplicationId): Either = either { + val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value) + userWalletsSyncDelegate.syncWallets(walletsInfo).bind() + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 37abe05472..8efcb31776 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsUM @@ -18,7 +19,12 @@ internal class PreviewDetailsComponent : DetailsComponent { private val previewBlocks = runBlocking { ItemsBuilder( router = DummyRouter(), - ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}, onBuyClick = {}) + ).buildAll( + isWalletConnectAvailable = true, + userWalletId = UserWalletId(""), + onSupportClick = {}, + onBuyClick = {}, + ) } private val previewFooter = DetailsFooterUM( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index d2068c9fcf..10afab1fde 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -80,6 +80,7 @@ internal class DetailsModel @Inject constructor( items = MutableStateFlow( itemsBuilder.buildAll( isWalletConnectAvailable = isWalletConnectAvailable, + userWalletId = params.userWalletId, onSupportClick = ::sendFeedback, onBuyClick = ::onBuyClick, ), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 7cd8933ad5..f0fd520172 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -6,6 +6,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.impl.BuildConfig import com.tangem.features.details.impl.R @@ -19,20 +20,21 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { fun buildAll( isWalletConnectAvailable: Boolean, + userWalletId: UserWalletId, onSupportClick: () -> Unit, onBuyClick: () -> Unit, ): ImmutableList = buildList { - buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) + buildWalletConnectBlock(isWalletConnectAvailable, userWalletId)?.let(::add) buildUserWalletListBlock().let(::add) buildShopBlock(onBuyClick).let(::add) buildSettingsBlock().let(::add) buildSupportBlock(onSupportClick).let(::add) }.toImmutableList() - private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { + private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? { return if (isWalletConnectAvailable) { DetailsItemUM.WalletConnect( - onClick = { router.push(AppRoute.WalletConnectSessions) }, + onClick = { router.push(AppRoute.WalletConnectSessions(userWalletId)) }, ) } else { null diff --git a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt index c7d60d1091..2b584ceaae 100644 --- a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt +++ b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt @@ -2,7 +2,9 @@ package com.tangem.features.walletconnect.components import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface WalletConnectEntryComponent : ComposableContentComponent { - interface Factory : ComponentFactory + data class Params(val userWalletId: UserWalletId) + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 02b082fd94..77ae59129c 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -12,9 +12,8 @@ android { } dependencies { + implementation(projects.features.wallet.api) implementation(projects.features.walletconnect.api) - implementation(projects.domain.walletConnect) - implementation(projects.domain.walletConnect.models) /** Core */ implementation(projects.core.configToggles) @@ -24,13 +23,22 @@ dependencies { implementation(projects.common.ui) /** Domain models */ + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.balanceHiding.models) implementation(projects.domain.blockaid.models) + implementation(projects.domain.models) implementation(projects.domain.qrScanning.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.walletConnect) + implementation(projects.domain.walletConnect.models) /** Domain */ + implementation(projects.domain.appCurrency) + implementation(projects.domain.balanceHiding) implementation(projects.domain.qrScanning) + implementation(projects.domain.tokens) + implementation(projects.domain.wallets) /** DI */ implementation(deps.hilt.android) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt index 6c6a74e65a..45e927da97 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt @@ -13,15 +13,17 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.connections.model.WcConnectionsModel import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetConfig import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent internal class ConnectionsComponent( appComponentContext: AppComponentContext, + params: Params, ) : AppComponentContext by appComponentContext, ComposableContentComponent { - private val model: WcConnectionsModel = getOrCreateModel() + private val model: WcConnectionsModel = getOrCreateModel(params) private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, serializer = null, @@ -52,4 +54,6 @@ internal class ConnectionsComponent( ) } } + + data class Params(val userWalletId: UserWalletId) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt index 3f3ee812d0..f6cce51fd1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt @@ -22,7 +22,7 @@ import dagger.assisted.AssistedInject @Suppress("UnusedPrivateMember") internal class DefaultWalletConnectEntryComponent @AssistedInject constructor( @Assisted private val appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Assisted private val params: WalletConnectEntryComponent.Params, ) : AppComponentContext by appComponentContext, WalletConnectEntryComponent { private val contentNavigation = StackNavigation() @@ -47,12 +47,18 @@ internal class DefaultWalletConnectEntryComponent @AssistedInject constructor( config: ConnectionsInnerRoute, componentContext: ComponentContext, ): ComposableContentComponent = when (config) { - ConnectionsInnerRoute.Connections -> ConnectionsComponent(childByContext(componentContext)) + ConnectionsInnerRoute.Connections -> ConnectionsComponent( + appComponentContext = childByContext(componentContext), + params = ConnectionsComponent.Params(params.userWalletId), + ) ConnectionsInnerRoute.QrScan -> TODO("[REDACTED_JIRA]") } @AssistedFactory interface Factory : WalletConnectEntryComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectEntryComponent + override fun create( + context: AppComponentContext, + params: WalletConnectEntryComponent.Params, + ): DefaultWalletConnectEntryComponent } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt index bcfbcc79ea..be95b71cd3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt @@ -1,13 +1,12 @@ package com.tangem.features.walletconnect.connections.components +import androidx.compose.animation.animateContentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext @@ -18,8 +17,10 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.connections.model.WcAppInfoModel import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.impl.R @@ -30,9 +31,8 @@ internal class WcAppInfoContainerComponent( ) : AppComponentContext by appComponentContext, ComposableContentComponent { private val model: WcAppInfoModel = getOrCreateModel(params = params) - private val contentNavigation = StackNavigation() private val contentStack = childStack( - source = contentNavigation, + source = model.contentNavigation, serializer = WcAppInfoRoutes.serializer(), initialConfiguration = WcAppInfoRoutes.AppInfo, childFactory = ::screenChild, @@ -51,10 +51,17 @@ internal class WcAppInfoContainerComponent( onDismissRequest = ::dismiss, content = content.active.configuration, ), - containerColor = TangemTheme.colors.background.tertiary, + onBack = ::contentBack, + containerColor = when (content.active.configuration) { + is WcAppInfoRoutes.Alert, + is WcAppInfoRoutes.SelectNetworks, + is WcAppInfoRoutes.AppInfo, + -> TangemTheme.colors.background.tertiary + is WcAppInfoRoutes.SelectWallet -> TangemTheme.colors.background.primary + }, title = { config -> Title(route = config) }, content = { - Children(stack = content, animation = stackAnimation()) { child -> + Children(modifier = Modifier.animateContentSize(), stack = content) { child -> child.instance.Content(modifier = Modifier) } }, @@ -64,7 +71,12 @@ internal class WcAppInfoContainerComponent( @Composable private fun Title(route: WcAppInfoRoutes) { TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = when (route) { + is WcAppInfoRoutes.Alert -> null + WcAppInfoRoutes.AppInfo -> resourceReference(R.string.wc_wallet_connect) + WcAppInfoRoutes.SelectNetworks -> TODO("[REDACTED_TASK_KEY]") + WcAppInfoRoutes.SelectWallet -> stringReference("Choose wallet") + }, startIconRes = when (route) { is WcAppInfoRoutes.AppInfo, is WcAppInfoRoutes.Alert -> null is WcAppInfoRoutes.SelectNetworks, is WcAppInfoRoutes.SelectWallet -> R.drawable.ic_back_24 @@ -88,19 +100,25 @@ internal class WcAppInfoContainerComponent( } private fun contentBack() { - contentNavigation.pop() + if (contentStack.value.active.configuration == WcAppInfoRoutes.AppInfo) { + dismiss() + } else { + model.contentNavigation.pop() + } } - private fun screenChild(config: WcAppInfoRoutes, componentContext: ComponentContext): ComposableContentComponent = - when (config) { - is WcAppInfoRoutes.AppInfo -> WcAppInfoComponent( - appComponentContext = childByContext(componentContext), - model = model, - ) + private fun screenChild(config: WcAppInfoRoutes, componentContext: ComponentContext): ComposableContentComponent { + val appComponentContext = childByContext(componentContext) + return when (config) { + is WcAppInfoRoutes.AppInfo -> WcAppInfoComponent(appComponentContext = appComponentContext, model = model) is WcAppInfoRoutes.Alert -> TODO() WcAppInfoRoutes.SelectNetworks -> TODO() - WcAppInfoRoutes.SelectWallet -> TODO() + WcAppInfoRoutes.SelectWallet -> WcSelectWalletComponent( + appComponentContext = appComponentContext, + model = model, + ) } + } - data class Params(val wcUrl: String, val source: WcPairRequest.Source) + data class Params(val userWalletId: UserWalletId, val wcUrl: String, val source: WcPairRequest.Source) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt new file mode 100644 index 0000000000..d6c7e19964 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt @@ -0,0 +1,130 @@ +package com.tangem.features.walletconnect.connections.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.walletconnect.connections.model.WcAppInfoModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class WcSelectWalletComponent( + appComponentContext: AppComponentContext, + private val model: WcAppInfoModel, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + @Composable + override fun Content(modifier: Modifier) { + val walletsState by model.walletsUiState.collectAsStateWithLifecycle() + WcSelectWalletContent( + modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + wallets = walletsState.wallets, + selectedWalletId = walletsState.selectedUserWallet.walletId, + ) + } +} + +@Composable +private fun WcSelectWalletContent( + wallets: ImmutableList, + selectedWalletId: UserWalletId, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + wallets.fastForEach { state -> + key(state.id) { + val baseModifier = Modifier + .clip(RoundedCornerShape(14.dp)) + .clickable(onClick = state.onClick) + val itemModifier = if (state.id == selectedWalletId) { + baseModifier.border( + width = 1.dp, + color = TangemTheme.colors.text.accent, + shape = RoundedCornerShape(14.dp), + ) + } else { + baseModifier + } + UserWalletItem( + modifier = itemModifier, + state = state, + blockColors = TangemBlockCardColors.copy( + containerColor = Color.Unspecified, + disabledContainerColor = Color.Unspecified, + ), + ) + } + } + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcSelectWalletContent_Preview() { + TangemThemePreview { + WcSelectWalletContent( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + selectedWalletId = UserWalletId("user_wallet_1".encodeToByteArray()), + wallets = persistentListOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_1".encodeToByteArray()), + name = stringReference("Tangem 2.0"), + information = stringReference("42 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_2".encodeToByteArray()), + name = stringReference("Tangem White"), + information = stringReference("24 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Bitcoin"), + information = stringReference("1 token"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + UserWalletItemUM( + id = UserWalletId("user_wallet_4".encodeToByteArray()), + name = stringReference("Tangem 1.0"), + information = stringReference("21 tokens"), + balance = UserWalletItemUM.Balance.Loaded("1 496,34 \$", isFlickering = false), + isEnabled = true, + onClick = {}, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index f7d17a2af5..92e385a3fa 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -26,6 +26,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { val appSubtitle: String, val notification: WcAppInfoSecurityNotification?, val walletName: String, + val onWalletClick: () -> Unit, val networksInfo: WcNetworksInfo, override val connectButtonConfig: WcPrimaryButtonConfig, override val onDismiss: () -> Unit, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoWalletUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoWalletUM.kt new file mode 100644 index 0000000000..0c682702a0 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoWalletUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.walletconnect.connections.entity + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.collections.immutable.ImmutableList + +internal data class WcAppInfoWalletUM( + val wallets: ImmutableList, + val selectedUserWallet: UserWallet, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt index 3a6e4520ba..fe9348ef4d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt @@ -1,47 +1,98 @@ package com.tangem.features.walletconnect.connections.model import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.pushNew +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.walletconnect.connections.components.WcAppInfoContainerComponent import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcAppInfoWalletUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoWalletChangedTransformer import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer +import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes +import com.tangem.features.walletconnect.connections.utils.WcUserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate +@Suppress("LongParameterList") @Stable @ModelScoped internal class WcAppInfoModel @Inject constructor( + private val router: Router, override val dispatchers: CoroutineDispatcherProvider, wcPairUseCaseFactory: WcPairUseCase.Factory, - private val router: Router, + getWalletsUseCase: GetWalletsUseCase, + getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + messageSender: UiMessageSender, + getCardImageUseCase: GetCardImageUseCase, paramsContainer: ParamsContainer, ) : Model() { private val params: WcAppInfoContainerComponent.Params = paramsContainer.require() - private val wcPairUseCase = wcPairUseCaseFactory.create(WcPairRequest(uri = params.wcUrl, source = params.source)) + private val wcPairUseCase = wcPairUseCaseFactory.create( + WcPairRequest( + userWalletId = params.userWalletId, + uri = params.wcUrl, + source = params.source, + ), + ) + private val userWalletsFetcher = WcUserWalletsFetcher( + getWalletsUseCase = getWalletsUseCase, + getWalletTotalBalanceUseCase = getWalletTotalBalanceUseCase, + getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase = getBalanceHidingSettingsUseCase, + messageSender = messageSender, + getCardImageUseCase = getCardImageUseCase, + onWalletSelected = ::onWalletSelected, + ) + + private val selectedUserWalletFlow = + MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) + // TODO(wc) Doston: Temp solution, will be fixed in next PR`s + private var proposalNetwork by Delegates.notNull() + internal var sessionProposal by Delegates.notNull() + + // UI states + internal val contentNavigation = StackNavigation() val appInfoUiState: StateFlow field = MutableStateFlow(createLoadingState()) - - // TODO(wc) Doston: Temp solution, will be fixed in next PR`s - private var userWallet by Delegates.notNull() - private var proposalNetwork by Delegates.notNull() + val walletsUiState: StateFlow + field = MutableStateFlow(WcAppInfoWalletUM(persistentListOf(), selectedUserWalletFlow.value)) init { loadDAppInfo() + combine( + flow = userWalletsFetcher.userWallets, + flow2 = selectedUserWalletFlow, + transform = ::updateWalletsState, + ).launchIn(modelScope) } private fun loadDAppInfo() { @@ -62,14 +113,15 @@ internal class WcAppInfoModel @Inject constructor( } is WcPairState.Loading -> appInfoUiState.update { createLoadingState() } is WcPairState.Proposal -> { - userWallet = state.dAppSession.proposalNetwork.keys.first { !it.isLocked } - proposalNetwork = state.dAppSession.proposalNetwork.getValue(userWallet) + sessionProposal = state.dAppSession + proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) appInfoUiState.transformerUpdate( WcAppInfoTransformer( dAppSession = state.dAppSession, onDismiss = ::dismiss, onConnect = ::onConnect, - userWallet = userWallet, + onWalletClick = { contentNavigation.pushNew(WcAppInfoRoutes.SelectWallet) }, + userWallet = selectedUserWalletFlow.value, proposalNetwork = proposalNetwork, ), ) @@ -87,13 +139,24 @@ internal class WcAppInfoModel @Inject constructor( private fun onConnect() { wcPairUseCase.approve( WcSessionApprove( - wallet = userWallet, - network = - proposalNetwork.required.plus(proposalNetwork.available).toList(), + wallet = selectedUserWalletFlow.value, + network = proposalNetwork.required.plus(proposalNetwork.available).toList(), ), ) } + private fun onWalletSelected(userWalletId: UserWalletId) { + val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } + selectedUserWalletFlow.update { selectedUserWallet } + proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWallet) + appInfoUiState.transformerUpdate(WcAppInfoWalletChangedTransformer(selectedUserWallet, proposalNetwork)) + contentNavigation.pop() + } + + private fun updateWalletsState(items: ImmutableList, userWallet: UserWallet) { + walletsUiState.update { state -> state.copy(wallets = items, selectedUserWallet = userWallet) } + } + private fun createLoadingState(): WcAppInfoUM.Loading { return WcAppInfoUM.Loading( onDismiss = ::dismiss, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 49062a9de9..96d5df7e28 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM @@ -16,11 +17,12 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase -import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase +import com.tangem.features.walletconnect.connections.components.ConnectionsComponent import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer @@ -44,8 +46,10 @@ internal class WcConnectionsModel @Inject constructor( private val wcDisconnectUseCase: WcDisconnectUseCase, private val wcPairService: WcPairService, override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, ) : Model() { + private val params = paramsContainer.require() val uiState: StateFlow field = MutableStateFlow(getInitialState()) val bottomSheetNavigation: SlotNavigation = SlotNavigation() @@ -58,7 +62,15 @@ internal class WcConnectionsModel @Inject constructor( private fun listenQrUpdates() { listenToQrScanningUseCase(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .onEach { wcUrl -> wcPairService.pair(WcPairRequest(wcUrl, WcPairRequest.Source.QR)) } + .onEach { wcUrl -> + wcPairService.pair( + WcPairRequest( + userWalletId = params.userWalletId, + uri = wcUrl, + source = WcPairRequest.Source.QR, + ), + ) + } .launchIn(modelScope) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 134338c410..2249504df5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -1,17 +1,16 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.domain.blockaid.models.dapp.CheckDAppResult -import com.tangem.core.ui.extensions.iconResId import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.entity.* import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.toImmutableList internal class WcAppInfoTransformer( private val dAppSession: WcSessionProposal, private val onDismiss: () -> Unit, private val onConnect: () -> Unit, + private val onWalletClick: () -> Unit, private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, ) : Transformer { @@ -24,7 +23,8 @@ internal class WcAppInfoTransformer( appSubtitle = dAppSession.dAppMetaData.description, notification = createNotification(dAppSession.securityStatus), walletName = userWallet.name, - networksInfo = convertNetworksInfo(proposalNetwork), + onWalletClick = onWalletClick, + networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), connectButtonConfig = WcPrimaryButtonConfig( showProgress = false, enabled = proposalNetwork.missingRequired.isEmpty(), @@ -41,26 +41,4 @@ internal class WcAppInfoTransformer( CheckDAppResult.FAILED_TO_VERIFY -> WcAppInfoSecurityNotification.UnknownDomain } } - - private fun convertNetworksInfo(proposalNetwork: WcSessionProposal.ProposalNetwork): WcNetworksInfo { - return if (proposalNetwork.missingRequired.isNotEmpty()) { - WcNetworksInfo.MissingRequiredNetworkInfo( - networks = proposalNetwork.missingRequired - .joinToString { it.name }, - ) - } else { - WcNetworksInfo.ContainsAllRequiredNetworks( - items = (proposalNetwork.required + proposalNetwork.available) - .map { - WcNetworkInfoItem( - id = it.id.value, - icon = it.iconResId, - name = it.name, - symbol = it.currencySymbol, - ) - } - .toImmutableList(), - ) - } - } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt new file mode 100644 index 0000000000..453ab9b608 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.utils.transformer.Transformer + +internal class WcAppInfoWalletChangedTransformer( + private val selectedUserWallet: UserWallet, + private val proposalNetwork: WcSessionProposal.ProposalNetwork, +) : Transformer { + override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { + val contentState = prevState as? WcAppInfoUM.Content ?: return prevState + return contentState.copy( + walletName = selectedUserWallet.name, + networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), + connectButtonConfig = prevState.connectButtonConfig.copy( + enabled = proposalNetwork.missingRequired.isEmpty(), + ), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt new file mode 100644 index 0000000000..8bf63c01cf --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.core.ui.extensions.iconResId +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem +import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +internal object WcNetworksInfoConverter : Converter { + override fun convert(value: WcSessionProposal.ProposalNetwork): WcNetworksInfo { + return if (value.missingRequired.isNotEmpty()) { + WcNetworksInfo.MissingRequiredNetworkInfo( + networks = value.missingRequired + .joinToString { it.name }, + ) + } else { + WcNetworksInfo.ContainsAllRequiredNetworks( + items = (value.required + value.available) + .map { + WcNetworkInfoItem( + id = it.id.value, + icon = it.iconResId, + name = it.name, + symbol = it.currencySymbol, + ) + } + .toImmutableList(), + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index cfe15122ed..6a424d83be 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -61,7 +61,11 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( ) is WcInnerRoute.Pair -> WcAppInfoContainerComponent( childContext, - WcAppInfoContainerComponent.Params(config.request.uri, config.request.source), + WcAppInfoContainerComponent.Params( + userWalletId = config.request.userWalletId, + wcUrl = config.request.uri, + source = config.request.source, + ), ) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt index f248d6323c..136d5ac26b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt @@ -220,7 +220,12 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier val itemsModifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12) - WalletRowItem(modifier = itemsModifier, walletName = state.walletName) + WalletRowItem( + modifier = Modifier + .clickable(onClick = state.onWalletClick) + .then(itemsModifier), + walletName = state.walletName, + ) HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) SelectNetworksBlock( networksInfo = state.networksInfo, @@ -565,6 +570,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider Unit, +) { + + private var loadedArtworks: HashMap = hashMapOf() + + @OptIn(ExperimentalCoroutinesApi::class) + val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> + val uiModels = UserWalletItemUMConverter(onClick = onWalletSelected) + .convertList(wallets) + .toImmutableList() + + emit(uiModels) + combine( + flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), + flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), + flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), + flow4 = loadArtworks(wallets), + ) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks -> + val models = createUiModels( + wallets = wallets, + maybeAppCurrency = maybeAppCurrency, + maybeBalances = maybeBalances, + balanceHidingSettings = balanceHidingSettings, + artworks = artworks, + ).getOrElse( + ifLoading = { return@combine }, + ifError = { + val message = resourceReference(R.string.common_unknown_error) + messageSender.send(SnackbarMessage(message)) + + return@combine + }, + ) + + emit(models) + }.collect() + } + + private fun loadArtworks(wallets: List): Flow> { + return flow { + emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish + wallets.forEach { wallet -> + val artwork = getCardImageUseCase( + cardId = wallet.cardId, + cardPublicKey = wallet.scanResponse.card.cardPublicKey, + size = ArtworkSize.SMALL, + ) + loadedArtworks[wallet.walletId] = artwork + emit(loadedArtworks) + } + } + } + + private fun createUiModels( + wallets: List, + maybeAppCurrency: Either, + maybeBalances: Lce>, + balanceHidingSettings: BalanceHidingSettings, + artworks: HashMap, + ): Lce> = lce { + val balances = withError( + transform = { Error.UnableToGetBalances }, + block = { + maybeBalances.bindOrNull().orEmpty() + .filterKeys { userWalletId -> wallets.any { it.walletId == userWalletId } } + .mapKeys { entry -> wallets.first { it.walletId == entry.key } } + }, + ) + + val appCurrency = withError( + transform = { Error.UnableToGetAppCurrency }, + block = { maybeAppCurrency.toLce().bind() }, + ) + + balances + .map { (userWallet, balance) -> + UserWalletItemUMConverter( + onClick = onWalletSelected, + appCurrency = appCurrency, + balance = balance, + isBalanceHidden = balanceHidingSettings.isBalanceHidden, + artwork = artworks[userWallet.walletId], + ) + .convert(userWallet) + } + .toImmutableList() + } + + sealed class Error { + + data object UnableToGetAppCurrency : Error() + + data object UnableToGetBalances : Error() + } +} \ No newline at end of file