Updated on 2026-08-14
This commit is contained in:
parent
0029b514bd
commit
3524a3bd86
7 changed files with 66 additions and 13 deletions
|
|
@ -25,6 +25,7 @@ import com.tangem.datasource.api.tangemTech.models.orDefault
|
||||||
import com.tangem.datasource.utils.getSyncOrNull
|
import com.tangem.datasource.utils.getSyncOrNull
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -143,6 +144,11 @@ internal class DefaultWalletAccountsFetcher @Inject constructor(
|
||||||
apiResponse.bind().enrichByAccountId()
|
apiResponse.bind().enrichByAccountId()
|
||||||
},
|
},
|
||||||
onError = { error ->
|
onError = { error ->
|
||||||
|
TangemLogger.e(
|
||||||
|
"pushInternal wallet=$userWalletId: PUT /accounts failed, " +
|
||||||
|
"isPreconditionFailed=${error.isNetworkError(code = Code.PRECONDITION_FAILED)}, error=$error",
|
||||||
|
)
|
||||||
|
|
||||||
if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) {
|
if (error.isNetworkError(code = Code.PRECONDITION_FAILED)) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||||
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
|
storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit,
|
||||||
): FetchResult {
|
): FetchResult {
|
||||||
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED)
|
||||||
|
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
|
||||||
|
|
||||||
if (isResponseUpToDate) {
|
if (isResponseUpToDate) {
|
||||||
TangemLogger.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
|
TangemLogger.e("ETag is up to date, no need to update accounts for wallet: $userWalletId")
|
||||||
val response = requireNotNull(savedAccountsResponse) {
|
val response = requireNotNull(savedAccountsResponse) {
|
||||||
|
|
@ -71,13 +73,16 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||||
val response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
|
val response = savedAccountsResponse ?: createDefaultResponse(userWalletId)
|
||||||
val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse()
|
val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse()
|
||||||
|
|
||||||
val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND)
|
|
||||||
if (isNotFoundError) {
|
if (isNotFoundError) {
|
||||||
val eTag = createWallet(userWalletId)
|
val eTag = createWallet(userWalletId)
|
||||||
|
|
||||||
if (eTag != null) {
|
if (eTag != null) {
|
||||||
pushWalletAccounts(accountDTOs, eTag)
|
pushWalletAccounts(accountDTOs, eTag)
|
||||||
userTokensSaver.pushWithRetryer(userWalletId, userTokensResponse)
|
userTokensSaver.pushWithRetryer(userWalletId, userTokensResponse)
|
||||||
|
} else {
|
||||||
|
TangemLogger.e(
|
||||||
|
"handle wallet=$userWalletId: account creation skipped, createWallet returned null eTag",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,10 +117,17 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor(
|
||||||
private suspend fun createWallet(userWalletId: UserWalletId): String? {
|
private suspend fun createWallet(userWalletId: UserWalletId): String? {
|
||||||
val creationResponse = walletServerBinder.bind(userWalletId)
|
val creationResponse = walletServerBinder.bind(userWalletId)
|
||||||
|
|
||||||
return if (creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED) {
|
val isCreated = creationResponse is ApiResponse.Success && creationResponse.code == Code.CREATED
|
||||||
|
val eTag = if (isCreated) {
|
||||||
creationResponse.headers[ETAG_HEADER]?.firstOrNull()
|
creationResponse.headers[ETAG_HEADER]?.firstOrNull()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eTag == null) {
|
||||||
|
TangemLogger.e("ETag is null for wallet: $userWalletId, isCreated: $isCreated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return eTag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.data.common.wallet
|
package com.tangem.data.common.wallet
|
||||||
|
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
import com.tangem.datasource.api.common.response.ETAG_HEADER
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter
|
||||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||||
|
|
@ -9,6 +10,7 @@ import com.tangem.domain.common.wallets.getSyncOrNull
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
internal class DefaultWalletServerBinder(
|
internal class DefaultWalletServerBinder(
|
||||||
|
|
@ -19,7 +21,12 @@ internal class DefaultWalletServerBinder(
|
||||||
) : WalletServerBinder {
|
) : WalletServerBinder {
|
||||||
|
|
||||||
override suspend fun bind(userWalletId: UserWalletId): ApiResponse<Unit>? {
|
override suspend fun bind(userWalletId: UserWalletId): ApiResponse<Unit>? {
|
||||||
val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) ?: return null
|
val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId)
|
||||||
|
|
||||||
|
if (userWallet == null) {
|
||||||
|
TangemLogger.e("bind wallet=$userWalletId: user wallet not found locally, skipping createWallet call")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return bind(userWallet)
|
return bind(userWallet)
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +38,12 @@ internal class DefaultWalletServerBinder(
|
||||||
tangemTechApi.createWallet(
|
tangemTechApi.createWallet(
|
||||||
body = WalletIdBodyConverter.convert(userWallet, conversionData),
|
body = WalletIdBodyConverter.convert(userWallet, conversionData),
|
||||||
)
|
)
|
||||||
|
}.also { response ->
|
||||||
|
val eTag = response.headers[ETAG_HEADER]?.firstOrNull()
|
||||||
|
TangemLogger.i(
|
||||||
|
"bind wallet=${userWallet.walletId}: createWallet code=${(response as? ApiResponse.Success)?.code}, " +
|
||||||
|
"hasETag=${eTag != null}, eTagNotEmpty=${!eTag.isNullOrEmpty()}",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,10 +11,11 @@ import com.tangem.core.navigation.finisher.AppFinisher
|
||||||
import com.tangem.feature.tester.impl.R
|
import com.tangem.feature.tester.impl.R
|
||||||
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
|
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
|
||||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggleGroupUM
|
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureToggleGroupUM
|
||||||
import com.tangem.feature.tester.presentation.featuretoggles.state.TesterFeatureToggleUM
|
|
||||||
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesScreenUM
|
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesScreenUM
|
||||||
|
import com.tangem.feature.tester.presentation.featuretoggles.state.TesterFeatureToggleUM
|
||||||
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
|
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
|
||||||
import com.tangem.utils.info.AppInfoProvider
|
import com.tangem.utils.info.AppInfoProvider
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -40,7 +41,7 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// Declared before `state` so it is initialized before `initState()` runs in the field initializer.
|
// Declared before `state` so it is initialized before `initState()` runs in the field initializer.
|
||||||
private val appVersion: Version? = Version.create(appInfoProvider.appVersion)
|
private val appVersion: Version? = createAppVersion()
|
||||||
|
|
||||||
val state: StateFlow<FeatureTogglesScreenUM>
|
val state: StateFlow<FeatureTogglesScreenUM>
|
||||||
field = MutableStateFlow(initState())
|
field = MutableStateFlow(initState())
|
||||||
|
|
@ -137,6 +138,20 @@ internal class FeatureTogglesViewModel @Inject constructor(
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createAppVersion(): Version? {
|
||||||
|
val rawVersion = appInfoProvider.appVersion
|
||||||
|
// Drop the build-type suffix (e.g. "6.0-internal" -> "6.0") so the version parses, mirroring
|
||||||
|
// DefaultVersionProvider; otherwise every toggle falls back to the "Not planned yet" group.
|
||||||
|
val sanitizedVersion = rawVersion.substringBefore(delimiter = '-')
|
||||||
|
val version = Version.create(sanitizedVersion)
|
||||||
|
|
||||||
|
if (version == null) {
|
||||||
|
TangemLogger.e("Failed to parse app version: raw=$rawVersion, sanitized=$sanitizedVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
private fun statusOf(toggleVersion: String): TesterFeatureToggleUM.Status {
|
private fun statusOf(toggleVersion: String): TesterFeatureToggleUM.Status {
|
||||||
val toggle = parseToggleVersion(toggleVersion) ?: return TesterFeatureToggleUM.Status.UNDEFINED
|
val toggle = parseToggleVersion(toggleVersion) ?: return TesterFeatureToggleUM.Status.UNDEFINED
|
||||||
val app = appVersion ?: return TesterFeatureToggleUM.Status.UNDEFINED
|
val app = appVersion ?: return TesterFeatureToggleUM.Status.UNDEFINED
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ internal interface WalletWarningsClickIntents {
|
||||||
|
|
||||||
fun onCloseAlreadySignedHashesWarningClick()
|
fun onCloseAlreadySignedHashesWarningClick()
|
||||||
|
|
||||||
fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>)
|
fun onGenerateMissedAddressesClick(userWalletId: UserWalletId, missedAddressCurrencies: List<CryptoCurrency>)
|
||||||
|
|
||||||
fun onOpenUnlockWalletsBottomSheetClick()
|
fun onOpenUnlockWalletsBottomSheetClick()
|
||||||
|
|
||||||
|
|
@ -164,19 +164,20 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGenerateMissedAddressesClick(missedAddressCurrencies: List<CryptoCurrency>) {
|
override fun onGenerateMissedAddressesClick(
|
||||||
|
userWalletId: UserWalletId,
|
||||||
|
missedAddressCurrencies: List<CryptoCurrency>,
|
||||||
|
) {
|
||||||
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped())
|
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped())
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
val userWallet = getSelectedUserWallet() ?: return@launch
|
|
||||||
|
|
||||||
derivePublicKeysUseCase(
|
derivePublicKeysUseCase(
|
||||||
userWalletId = userWallet.walletId,
|
userWalletId = userWalletId,
|
||||||
currencies = missedAddressCurrencies,
|
currencies = missedAddressCurrencies,
|
||||||
).fold(
|
).fold(
|
||||||
ifLeft = { TangemLogger.e("Failed to derive public keys", it) },
|
ifLeft = { TangemLogger.e("Failed to derive public keys", it) },
|
||||||
ifRight = {
|
ifRight = {
|
||||||
fetchCryptoCurrencies(userWalletId = userWallet.walletId, currencies = missedAddressCurrencies)
|
fetchCryptoCurrencies(userWalletId = userWalletId, currencies = missedAddressCurrencies)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
tangemIcon = walletInterationIcon(userWallet),
|
tangemIcon = walletInterationIcon(userWallet),
|
||||||
missingAddressesCount = currencies.count(),
|
missingAddressesCount = currencies.count(),
|
||||||
onGenerateClick = {
|
onGenerateClick = {
|
||||||
clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies)
|
clickIntents.onGenerateMissedAddressesClick(
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
|
missedAddressCurrencies = currencies,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
condition = currencies.isNotEmpty(),
|
condition = currencies.isNotEmpty(),
|
||||||
|
|
|
||||||
|
|
@ -215,7 +215,10 @@ internal class GetWalletNotificationsFactory @Inject constructor(
|
||||||
tangemIcon = walletInterationIcon(userWallet),
|
tangemIcon = walletInterationIcon(userWallet),
|
||||||
missingAddressesCount = currencies.count(),
|
missingAddressesCount = currencies.count(),
|
||||||
onGenerateClick = {
|
onGenerateClick = {
|
||||||
clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies)
|
clickIntents.onGenerateMissedAddressesClick(
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
|
missedAddressCurrencies = currencies,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
condition = currencies.isNotEmpty(),
|
condition = currencies.isNotEmpty(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue