Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-16 14:50:24 +01:00
commit 74239690e8
22 changed files with 160 additions and 51 deletions

View file

@ -139,8 +139,9 @@ class DialogManager : StoreSubscriber<GlobalState> {
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
message = context.getString(
state.dialog.messageRes,
state.dialog.currencySymbol,
state.dialog.currencyTitle,
state.dialog.currencySymbol,
state.dialog.networkName,
),
context = context,
)

View file

@ -43,6 +43,7 @@ sealed class AppDialog : StateDialog {
data class TokensAreLinkedDialog(
val currencyTitle: String,
val currencySymbol: String,
val networkName: String,
) : AppDialog() {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title

View file

@ -30,7 +30,7 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
when {
// Destroy the activity if it invoked for the same activity. Prevents overwriting to null if there is a
// new scan from the background [REDACTED_TASK_KEY]
navState.activity?.get() == navigationAction.activity.get() -> navState.copy(activity = null)
navState.activity?.get() == navigationAction.activity.get() -> NavigationState()
else -> navState
}
}

View file

@ -19,6 +19,8 @@ import com.tangem.sdk.extensions.*
import com.tangem.sdk.nfc.NfcManager
import com.tangem.sdk.storage.create
import com.tangem.tap.foregroundActivityObserver
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
import javax.inject.Singleton
@ -30,6 +32,7 @@ import javax.inject.Singleton
@Singleton
internal class DefaultCardSdkProvider @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
) : CardSdkProvider, CardSdkOwner {
private val observer = Observer()
@ -39,12 +42,12 @@ internal class DefaultCardSdkProvider @Inject constructor(
override val sdk: TangemSdk
get() = holder?.sdk ?: tryToRegisterWithForegroundActivity()
override fun register(activity: FragmentActivity) {
override fun register(activity: FragmentActivity) = runBlocking(dispatchers.mainImmediate) {
if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) {
val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing"
analyticsEventHandler.send(TangemSdkWarningEvent(message))
Log.info { message }
return
return@runBlocking
}
if (holder != null) {
@ -58,7 +61,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
Log.info { "Tangem SDK owner registered" }
}
private fun tryToRegisterWithForegroundActivity(): TangemSdk {
private fun tryToRegisterWithForegroundActivity(): TangemSdk = runBlocking(dispatchers.mainImmediate) {
val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity"
analyticsEventHandler.send(TangemSdkWarningEvent(warning))
Log.warning { warning }
@ -83,7 +86,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
error(error)
}
return sdk
return@runBlocking sdk
}
private fun initialize(activity: FragmentActivity) {

View file

@ -33,6 +33,10 @@ interface TangemSdkManager {
val userCodeRequestPolicy: UserCodeRequestPolicy
suspend fun checkCanUseBiometry(awaitInitialization: Boolean = true): Boolean
suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean = true): Boolean
suspend fun scanProduct(
cardId: String? = null,
messageRes: Int? = null,

View file

@ -3,9 +3,11 @@ package com.tangem.tap.domain.sdk.impl
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.tangem.Log
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.common.*
import com.tangem.common.authentication.AuthenticationManager
import com.tangem.common.authentication.keystore.KeystoreManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.*
@ -38,16 +40,21 @@ import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
class DefaultTangemSdkManager(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val resources: Resources,
) : TangemSdkManager {
private val awaitInitializationMutex = Mutex()
private val tangemSdk: TangemSdk
get() = cardSdkConfigRepository.sdk
@ -73,6 +80,42 @@ class DefaultTangemSdkManager(
override val userCodeRequestPolicy: UserCodeRequestPolicy
get() = tangemSdk.config.userCodeRequestPolicy
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
return try {
needEnrollBiometrics
} catch (e: TangemSdkError.AuthenticationNotInitialized) {
Log.error {
"Trying to access `needEnrollBiometrics` flag when authentication manager is not initialized: " +
if (awaitInitialization) "awaiting initialization" else "failing"
}
if (awaitInitialization) {
awaitAuthenticationManagerInitialization().needEnrollBiometrics
} else {
throw e
}
}
}
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean {
return try {
canUseBiometry
} catch (e: TangemSdkError.AuthenticationNotInitialized) {
Log.error {
"Trying to access `canUseBiometry` flag when authentication manager is not initialized: " +
if (awaitInitialization) "awaiting initialization" else "failing"
}
if (awaitInitialization) {
val manager = awaitAuthenticationManagerInitialization()
manager.canAuthenticate || manager.needEnrollBiometrics
} else {
throw e
}
}
}
override suspend fun scanProduct(
cardId: String?,
messageRes: Int?,
@ -288,6 +331,26 @@ class DefaultTangemSdkManager(
tangemSdk.config.userCodeRequestPolicy = policy
}
private suspend fun awaitAuthenticationManagerInitialization(): AuthenticationManager {
return awaitInitializationMutex.withLock {
var attemps = 0
do {
if (tangemSdk.authenticationManager.isInitialized) {
break
} else {
if (attemps++ >= MAX_INITIALIZE_ATTEMPTS) {
error("Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts")
} else {
delay(timeMillis = 200)
}
}
} while (true)
tangemSdk.authenticationManager
}
}
// region Twin-specific
override suspend fun createFirstTwinWallet(
@ -335,6 +398,8 @@ class DefaultTangemSdkManager(
// endregion
companion object {
private const val MAX_INITIALIZE_ATTEMPTS = 10
@Deprecated("Use [DefaultCardSdkProvider] instead")
val config = Config(
linkedTerminal = true,

View file

@ -28,9 +28,11 @@ class MockTangemSdkManager(
private val resources: Resources,
) : TangemSdkManager {
override val canUseBiometry = false
private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default
override val needEnrollBiometrics = false
override val canUseBiometry: Boolean = false
override val needEnrollBiometrics: Boolean = false
override val keystoreManager = DummyKeystoreManager()
@ -39,7 +41,9 @@ class MockTangemSdkManager(
override val userCodeRequestPolicy: UserCodeRequestPolicy
get() = userCodeRequestPolicyInternal
private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean = canUseBiometry
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean = needEnrollBiometrics
override suspend fun scanProduct(
cardId: String?,

View file

@ -7,5 +7,5 @@ internal class DefaultLegacySettingsRepository(
private val tangemSdkManager: TangemSdkManager,
) : LegacySettingsRepository {
override fun canUseBiometry(): Boolean = tangemSdkManager.canUseBiometry
override suspend fun canUseBiometry(): Boolean = tangemSdkManager.checkCanUseBiometry()
}

View file

@ -37,7 +37,10 @@ import com.tangem.utils.coroutines.saveIn
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
@ -265,7 +268,6 @@ class DetailsMiddleware {
.onEach { needEnrollBiometrics ->
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
}
.flowOn(Dispatchers.IO)
.launchIn(lifecycleScope)
.saveIn(checkBiometricsStatusJobHolder)
}

View file

@ -76,7 +76,9 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
},
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appSettingsState = AppSettingsState(
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
isBiometricsAvailable = runBlocking {
tangemSdkManager.checkCanUseBiometry()
},
saveWallets = action.shouldSaveUserWallets,
saveAccessCodes = runBlocking {
store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()

View file

@ -99,7 +99,7 @@ object OnboardingHelper {
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.canUseBiometry && settingsRepository.shouldShowSaveUserWalletScreen() -> {
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
delay(timeMillis = 1_200)

View file

@ -91,10 +91,10 @@ internal class SaveWalletMiddleware {
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
}
private fun allowToUseBiometrics(state: SaveWalletState) {
if (tangemSdkManager.needEnrollBiometrics) {
store.dispatchOnMain(SaveWalletAction.EnrollBiometrics)
return
private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch {
if (tangemSdkManager.checkNeedEnrollBiometrics()) {
store.dispatchWithMain(SaveWalletAction.EnrollBiometrics)
return@launch
}
if (state.backupInfo != null) {
@ -104,24 +104,22 @@ internal class SaveWalletMiddleware {
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
}
scope.launch {
/*
/*
* because it will be automatically saved on UserWalletsListManager switch
*/
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
val error = IllegalStateException("No selected user wallet")
Timber.e(error, "Unable to save user wallet")
store.dispatchWithMain(
SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)),
)
return@launch
}
* because it will be automatically saved on UserWalletsListManager switch
*/
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
val error = IllegalStateException("No selected user wallet")
Timber.e(error, "Unable to save user wallet")
store.dispatchWithMain(
SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)),
)
return@launch
}
handleSuccessAllowing(selectedUserWallet)
}.saveIn(saveWalletJobHolder)
}
handleSuccessAllowing(selectedUserWallet)
}.saveIn(saveWalletJobHolder)
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)

View file

@ -29,9 +29,13 @@ internal class DefaultTokensListRouter : TokensListRouter {
store.dispatchNotification(R.string.contract_address_copied_message)
}
override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String) {
override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String) {
store.dispatchDialogShow(
dialog = AppDialog.TokensAreLinkedDialog(currencyTitle = tokenName, currencySymbol = tokenSymbol),
dialog = AppDialog.TokensAreLinkedDialog(
currencyTitle = tokenName,
currencySymbol = tokenSymbol,
networkName = networkName,
),
)
}

View file

@ -19,8 +19,9 @@ internal interface TokensListRouter {
*
* @param tokenName token name
* @param tokenSymbol token brief name
* @param networkName blockchain network full name
*/
fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String)
fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String)
/**
* Open alert to remove wallet

View file

@ -365,6 +365,7 @@ internal class TokensListViewModel @Inject constructor(
router.openUnableHideMainTokenAlert(
tokenName = blockchain.name,
tokenSymbol = blockchain.currency,
networkName = blockchain.fullName,
)
} else if (isAddedOnMainScreen) {
router.openRemoveWalletAlert(

View file

@ -5,6 +5,7 @@ import okio.Timeout
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
internal class ApiResponseCallDelegate<T : Any>(
private val wrappedCall: Call<T>,
@ -20,7 +21,9 @@ internal class ApiResponseCallDelegate<T : Any>(
override fun timeout(): Timeout = wrappedCall.timeout()
override fun isExecuted(): Boolean = wrappedCall.isExecuted
override fun isCanceled(): Boolean = wrappedCall.isCanceled
override fun cancel() { wrappedCall.cancel() }
override fun cancel() {
wrappedCall.cancel()
}
private inner class ApiResponseCallback(
private val responseCallback: Callback<ApiResponse<T>>,
@ -33,7 +36,15 @@ internal class ApiResponseCallDelegate<T : Any>(
}
override fun onFailure(call: Call<T>, t: Throwable) {
val error = t.toApiError()
val error = try {
t.toApiError()
} catch (e: ApiResponseError) {
Timber.e(e, "error map toApiError")
e
} catch (e: Exception) {
Timber.e(e, "onFailure UnknownException")
ApiResponseError.UnknownException(e)
}
val safeResponse = apiError<T>(error)
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))

View file

@ -73,10 +73,16 @@ sealed class ApiResponseError : Exception() {
}
/** Represents a network error, typically when there's no connectivity. */
data object NetworkException : ApiResponseError()
@Suppress("UnusedPrivateMember")
data object NetworkException : ApiResponseError() {
private fun readResolve(): Any = NetworkException
}
/** Represents a timeout error, typically when the server takes too long to respond. */
data object TimeoutException : ApiResponseError()
@Suppress("UnusedPrivateMember")
data object TimeoutException : ApiResponseError() {
private fun readResolve(): Any = TimeoutException
}
/**
* Represents an unexpected exception that doesn't fall into one of the other categories.

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.response
import kotlinx.coroutines.TimeoutCancellationException
import retrofit2.Response
import timber.log.Timber
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
@ -16,10 +17,15 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
} else {
val code = ApiResponseError.HttpException.Code.values
.firstOrNull { it.code == code() }
val e = if (code == null) {
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
} else {
ApiResponseError.HttpException(code, message(), errorBody()?.string())
val e = try {
if (code == null) {
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
} else {
ApiResponseError.HttpException(code, message(), errorBody()?.string())
}
} catch (e: Exception) {
Timber.e(e, "UnknownException occured")
ApiResponseError.UnknownException(e)
}
apiError(e)

View file

@ -172,7 +172,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid
title = resourceReference(id = R.string.main_swap_changelly_promotion_title),
subtitle = resourceReference(
id = R.string.main_swap_changelly_promotion_message,
formatArgs = wrappedList("1", "2"),
formatArgs = wrappedList("1", "2", "3"),
),
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_blue_banner_background,
@ -180,7 +180,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid
NotificationConfig(
title = resourceReference(
id = R.string.token_swap_changelly_promotion_title,
formatArgs = wrappedList("1", "2"),
formatArgs = wrappedList("1"),
),
subtitle = stringReference(
"Swap multiple currencies between any chains you wish. Swap multiple " +

View file

@ -4,5 +4,5 @@ import com.tangem.domain.settings.repositories.LegacySettingsRepository
class CanUseBiometryUseCase(private val legacySettingsRepository: LegacySettingsRepository) {
operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry()
suspend operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry()
}

View file

@ -2,5 +2,5 @@ package com.tangem.domain.settings.repositories
interface LegacySettingsRepository {
fun canUseBiometry(): Boolean
suspend fun canUseBiometry(): Boolean
}

View file

@ -89,7 +89,7 @@ markdown = "0.7.2"
# region Tangem
tangemBlockchainSdk = "release-app_5.12-698"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.12-369"
tangemCardSdk = "release-app_5.12-371"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem