Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-06 13:15:52 +03:00
commit 5fc4ff6837
50 changed files with 887 additions and 198 deletions

View file

@ -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,
)
}
}

View file

@ -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),

View file

@ -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)

View file

@ -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)

View file

@ -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 }
}

View file

@ -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(

View file

@ -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<WalletConnectState> {
private val model: WalletConnectModel = getOrCreateModel()
private val model: WalletConnectModel = getOrCreateModel(params)
private var screenState: MutableState<WalletConnectScreenState> =
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
}
}

View file

@ -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<WalletConnectComponent.Params>()
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) }
}
}

View file

@ -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<Unit, WalletConnectComponent>
data class Params(val userWalletId: UserWalletId)
interface Factory : ComponentFactory<Params, WalletConnectComponent>
}

View file

@ -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,
)
}

View file

@ -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}") {

View file

@ -172,6 +172,9 @@ interface TangemTechApi {
@GET("user-wallets/wallets/{wallet_id}")
suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse<WalletResponse>
@GET("user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
companion object {

View file

@ -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,
),

View file

@ -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<Preferences>(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<Preferences>(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,
),
)
}
}

View file

@ -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

View file

@ -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<UserWalletId, SeedPhraseNotificationsStatus>
@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<UserWalletRemoteInfo> =
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

View file

@ -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<WalletResponse, UserWalletRemoteInfo> {
override fun convert(value: WalletResponse): UserWalletRemoteInfo {
return UserWalletRemoteInfo(
walletId = UserWalletId(value.id),
name = value.name.orEmpty(),
isNotificationsEnabled = value.notifyStatus,
)
}
}

View file

@ -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<Preferences>()
// 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()) }
}
}

View file

@ -0,0 +1,4 @@
package com.tangem.domain.notifications.models
@JvmInline
value class ApplicationId(val value: String)

View file

@ -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<Throwable, String> = Either.catch {
suspend operator fun invoke(): Either<Throwable, ApplicationId> = Either.catch {
val localApplicationId = notificationsRepository.getApplicationId()
if (localApplicationId != null) return@catch localApplicationId

View file

@ -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<String>)
@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<NotificationsEligibleNetwork>

View file

@ -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 {

View file

@ -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)

View file

@ -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 }
}

View file

@ -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
}

View file

@ -0,0 +1,7 @@
package com.tangem.domain.wallets.models
class UserWalletRemoteInfo(
val walletId: UserWalletId,
val name: String,
val isNotificationsEnabled: Boolean,
)

View file

@ -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<UpdateWalletError, UserWallet> {
return renameUserWallet(userWalletId, name)
}
override suspend fun syncWallets(list: List<UserWalletRemoteInfo>): Either<UpdateWalletError, Unit> = either {
list.forEach { userWallet ->
renameUserWallet(userWallet.walletId, userWallet.name).bind()
}
}
private suspend fun renameUserWallet(
userWalletId: UserWalletId,
name: String,
): Either<UpdateWalletError, UserWallet> = 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
}
}
}
}

View file

@ -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<UpdateWalletError, UserWallet>
suspend fun syncWallets(list: List<UserWalletRemoteInfo>): Either<UpdateWalletError, Unit>
}

View file

@ -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<UserWalletRemoteInfo>
}

View file

@ -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<UpdateWalletError, UserWallet> =
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()
}
}

View file

@ -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<UpdateWalletError, Unit> = either {
val walletsInfo = walletsRepository.getWalletsInfo(applicationId.value)
userWalletsSyncDelegate.syncWallets(walletsInfo).bind()
}
}

View file

@ -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(

View file

@ -80,6 +80,7 @@ internal class DetailsModel @Inject constructor(
items = MutableStateFlow(
itemsBuilder.buildAll(
isWalletConnectAvailable = isWalletConnectAvailable,
userWalletId = params.userWalletId,
onSupportClick = ::sendFeedback,
onBuyClick = ::onBuyClick,
),

View file

@ -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<DetailsItemUM> = 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

View file

@ -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<Unit, WalletConnectEntryComponent>
data class Params(val userWalletId: UserWalletId)
interface Factory : ComponentFactory<Params, WalletConnectEntryComponent>
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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<ConnectionsInnerRoute>()
@ -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
}
}

View file

@ -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<WcAppInfoRoutes>()
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)
}

View file

@ -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<UserWalletItemUM>,
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 = {},
),
),
)
}
}

View file

@ -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,

View file

@ -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<UserWalletItemUM>,
val selectedUserWallet: UserWallet,
)

View file

@ -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<WcSessionProposal.ProposalNetwork>()
internal var sessionProposal by Delegates.notNull<WcSessionProposal>()
// UI states
internal val contentNavigation = StackNavigation<WcAppInfoRoutes>()
val appInfoUiState: StateFlow<WcAppInfoUM>
field = MutableStateFlow<WcAppInfoUM>(createLoadingState())
// TODO(wc) Doston: Temp solution, will be fixed in next PR`s
private var userWallet by Delegates.notNull<UserWallet>()
private var proposalNetwork by Delegates.notNull<WcSessionProposal.ProposalNetwork>()
val walletsUiState: StateFlow<WcAppInfoWalletUM>
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<UserWalletItemUM>, userWallet: UserWallet) {
walletsUiState.update { state -> state.copy(wallets = items, selectedUserWallet = userWallet) }
}
private fun createLoadingState(): WcAppInfoUM.Loading {
return WcAppInfoUM.Loading(
onDismiss = ::dismiss,

View file

@ -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<ConnectionsComponent.Params>()
val uiState: StateFlow<WcConnectionsState>
field = MutableStateFlow<WcConnectionsState>(getInitialState())
val bottomSheetNavigation: SlotNavigation<WcConnectionsBottomSheetConfig> = 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)
}

View file

@ -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<WcAppInfoUM> {
@ -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(),
)
}
}
}

View file

@ -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<WcAppInfoUM> {
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(),
),
)
}
}

View file

@ -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<WcSessionProposal.ProposalNetwork, WcNetworksInfo> {
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(),
)
}
}
}

View file

@ -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,
),
)
}
}

View file

@ -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<WcAppI
appSubtitle = "react-app.walletconnect.com",
notification = WcAppInfoSecurityNotification.SecurityRisk,
walletName = "Tangem 2.0",
onWalletClick = {},
networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks(
items = persistentListOf(
WcNetworkInfoItem(
@ -604,6 +610,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
appSubtitle = "react-app.walletconnect.com",
notification = WcAppInfoSecurityNotification.UnknownDomain,
walletName = "Tangem 2.0",
onWalletClick = {},
networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(networks = "Solana"),
onDismiss = {},
connectButtonConfig = WcPrimaryButtonConfig(showProgress = false, enabled = true, onClick = {}),

View file

@ -0,0 +1,137 @@
package com.tangem.features.walletconnect.connections.utils
import arrow.core.Either
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TotalFiatBalance
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.impl.R
import com.tangem.operations.attestation.ArtworkSize
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
@Suppress("LongParameterList")
@ModelScoped
internal class WcUserWalletsFetcher(
getWalletsUseCase: GetWalletsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val messageSender: UiMessageSender,
private val getCardImageUseCase: GetCardImageUseCase,
private val onWalletSelected: (UserWalletId) -> Unit,
) {
private var loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
@OptIn(ExperimentalCoroutinesApi::class)
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = 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<UserWallet>): Flow<HashMap<UserWalletId, ArtworkModel>> {
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<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
artworks: HashMap<UserWalletId, ArtworkModel>,
): Lce<Error, ImmutableList<UserWalletItemUM>> = 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()
}
}