Updated on 2026-08-14

This commit is contained in:
Tangem 2024-03-27 16:07:22 +00:00
commit dc49208ccc
35 changed files with 277 additions and 235 deletions

View file

@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
@ -38,7 +37,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
signInState = SignInReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)
}

View file

@ -29,8 +29,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -56,7 +54,6 @@ data class AppState(
val walletConnectState: WalletConnectState = WalletConnectState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val signInState: SignInState = SignInState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -93,7 +90,6 @@ data class AppState(
SaveWalletMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
SignInMiddleware.middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,
TradeCryptoMiddleware.middleware,

View file

@ -4,7 +4,7 @@ import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.feedback.ScanFailsEmail
import com.tangem.tap.common.redux.global.GlobalAction
@ -20,7 +20,7 @@ object ScanFailsDialog {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
Analytics.send(IntroductionProcess.ButtonRequestSupport())
Analytics.send(Basic.ButtonSupport())
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
}
setNeutralButton(R.string.common_cancel) { _, _ -> }

View file

@ -122,34 +122,38 @@ internal class GeneralUserWalletsListManager(
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
.distinctUntilChanged()
.onEach { shouldSaveUserWallets ->
val manager = if (shouldSaveUserWallets) {
biometricUserWalletsListManager.copyFrom(runtimeUserWalletsListManager)
val possibleManager = if (shouldSaveUserWallets) {
biometricUserWalletsListManager
} else {
runtimeUserWalletsListManager.copyFrom(biometricUserWalletsListManager)
runtimeUserWalletsListManager
}
Timber.d("Switch to ${manager::class.java.simpleName}")
if (possibleManager == implementation.value) {
Timber.e("Switch to same manager ${possibleManager::class.simpleName}")
}
implementation.value = manager
Timber.d("Switch to ${possibleManager::class.simpleName}")
clearOldManager(manager)
val previousManager = implementation.value
implementation.value = copySelectedUserWallet(
sourceManager = previousManager,
destinationManager = possibleManager,
)
previousManager.clear()
}
.flowOn(dispatchers.io)
.launchIn(applicationScope)
}
/** Copy data from [old] manager */
private suspend fun UserWalletsListManager.copyFrom(old: UserWalletsListManager): UserWalletsListManager {
old.selectedUserWalletSync?.let { this.save(it) }
return this
}
private suspend fun clearOldManager(current: UserWalletsListManager) {
if (current == biometricUserWalletsListManager) {
runtimeUserWalletsListManager.clear()
} else {
biometricUserWalletsListManager.clear()
private suspend fun copySelectedUserWallet(
sourceManager: UserWalletsListManager,
destinationManager: UserWalletsListManager,
): UserWalletsListManager {
sourceManager.selectedUserWalletSync?.let { selectedWallet ->
destinationManager.save(selectedWallet, canOverride = true)
}
return destinationManager
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.session.WCSession
import timber.log.Timber
import java.io.FileNotFoundException
import java.nio.charset.Charset
class WalletConnectRepository(val context: Application) {
@ -35,6 +36,8 @@ class WalletConnectRepository(val context: Application) {
val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS)
.hexToUtf8()
walletConnectAdapter.fromJson(json)!!.map { it.toSession() }
} catch (e: FileNotFoundException) {
emptyList()
} catch (exception: Exception) {
Timber.w(exception)
emptyList()

View file

@ -14,12 +14,9 @@ import com.walletconnect.android.CoreClient
import com.walletconnect.android.relay.ConnectionType
import com.walletconnect.web3.wallet.client.Wallet
import com.walletconnect.web3.wallet.client.Web3Wallet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -192,8 +189,8 @@ class WalletConnectRepositoryImpl @Inject constructor(
if (sessionDelete is Wallet.Model.SessionDelete.Success) {
scope.launch {
_events.emit(WalletConnectEvents.SessionDeleted(sessionDelete.topic))
updateSessionsInternal().join()
}
updateSessions()
}
Timber.d("onSessionDelete: $sessionDelete")
}
@ -209,21 +206,21 @@ class WalletConnectRepositoryImpl @Inject constructor(
accounts = userNamespaces?.flatMap { it.value } ?: emptyList(),
),
)
updateSessionsInternal().join()
}
updateSessions()
}
}
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
// Triggered when wallet receives the session update response from Dapp
Timber.d("onSessionUpdateResponse: $sessionUpdateResponse")
updateSessions()
updateSessionsInternal()
}
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
// Triggered whenever the connection state is changed
Timber.d("onConnectionStateChange: $state")
if (state.isAvailable) updateSessions()
if (state.isAvailable) updateSessionsInternal()
}
override fun onError(error: Wallet.Model.Error) {
@ -416,7 +413,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
dAppUrl = session?.url ?: "",
),
)
updateSessions()
updateSessionsInternal()
Timber.d("Disconnected successfully: $it")
},
onError = {
@ -440,20 +437,22 @@ class WalletConnectRepositoryImpl @Inject constructor(
}
override fun updateSessions() {
scope.launch {
val availableSessions = Web3Wallet.getListOfActiveSessions()
.map {
WalletConnectSession(
topic = it.topic,
icon = it.metaData?.icons?.firstOrNull(),
name = it.metaData?.name,
url = it.metaData?.url,
)
}
Timber.d("Available sessions: $availableSessions")
currentSessions = availableSessions
_activeSessions.emit(availableSessions)
}
updateSessionsInternal()
}
private fun updateSessionsInternal(): Job = scope.launch {
val availableSessions = Web3Wallet.getListOfActiveSessions()
.map {
WalletConnectSession(
topic = it.topic,
icon = it.metaData?.icons?.firstOrNull(),
name = it.metaData?.name,
url = it.metaData?.url,
)
}
Timber.d("Available sessions: $availableSessions")
currentSessions = availableSessions
_activeSessions.emit(availableSessions)
}
private fun findMissingNetworks(

View file

@ -37,10 +37,12 @@ class WalletConnectInteractor(
suspend fun startListening(userWalletId: String, cardId: String?) {
this.userWalletId = userWalletId
this.cardId = cardId
walletConnectRepository.updateSessions()
coroutineScope {
launch { subscribeToEvents() }
launch { subscribeToSessions() }
walletConnectRepository.updateSessions()
}
}
@ -88,11 +90,12 @@ class WalletConnectInteractor(
is WalletConnectEvents.SessionApprovalSuccess -> {
sessionsRepository.saveSession(
userWallet = userWalletId,
session = Session.fromAccounts(
session = Session(
accounts = wcEvent.accounts,
topic = wcEvent.topic,
),
)
walletConnectRepository.updateSessions()
handler.onSessionEstablished()
}
is WalletConnectEvents.SessionDeleted -> {

View file

@ -3,13 +3,4 @@ package com.tangem.tap.domain.walletconnect2.domain.models
data class Session(
val topic: String,
val accounts: List<Account>,
) {
companion object {
fun fromAccounts(accounts: List<Account>, topic: String): Session {
return Session(
topic = topic,
accounts = accounts,
)
}
}
}
)

View file

@ -20,7 +20,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.isLockedSync
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
@ -39,6 +39,7 @@ 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.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
@ -81,6 +82,7 @@ class DetailsMiddleware {
}
class EraseWalletMiddleware {
@Suppress("CyclomaticComplexMethod")
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
is DetailsAction.ResetToFactory.Start -> {
@ -119,8 +121,12 @@ class DetailsMiddleware {
doBeforeErase()
tangemSdkManager.resetToFactorySettings(card.cardId, true)
.flatMap { userWalletsListManager.delete(listOfNotNull(userWalletId)) }
.flatMap { tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) }
.flatMap {
userWalletsListManager.delete(listOfNotNull(userWalletId))
}
.flatMap {
tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId))
}
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
@ -129,7 +135,9 @@ class DetailsMiddleware {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
} else {
if (userWalletsListManager.isLockedSync) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
@ -217,7 +225,7 @@ class DetailsMiddleware {
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
observeBiometricsStatusChanges(state, action.lifecycleScope)
observeBiometricsStatusChanges(action.lifecycleScope)
}
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
@ -239,20 +247,27 @@ class DetailsMiddleware {
}
}
private fun observeBiometricsStatusChanges(state: DetailsState, lifecycleScope: LifecycleCoroutineScope) {
lifecycleScope.launch(Dispatchers.IO) {
private fun observeBiometricsStatusChanges(lifecycleScope: LifecycleCoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {
val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
if (needEnrollBiometrics != null &&
needEnrollBiometrics != state.appSettingsState.needEnrollBiometrics
) {
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
if (needEnrollBiometrics != null) {
emit(needEnrollBiometrics)
}
delay(timeMillis = 500)
delay(timeMillis = 200)
} while (true)
}.saveIn(checkBiometricsStatusJobHolder)
}
needEnrollBiometricsFlow
.distinctUntilChanged()
.onEach { needEnrollBiometrics ->
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
}
.flowOn(Dispatchers.IO)
.launchIn(lifecycleScope)
.saveIn(checkBiometricsStatusJobHolder)
}
private fun enrollBiometrics() {

View file

@ -59,8 +59,7 @@ sealed class WalletConnectAction : Action {
) : WalletConnectAction()
data class FailureEstablishingSession(val session: WCSession?, val error: TapError? = null) : WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :
WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) : WalletConnectAction()
data class DisconnectSession(val topic: String, val session: WCSession?) : WalletConnectAction()

View file

@ -20,8 +20,9 @@ object WalletConnectReducer {
is WalletConnectAction.SetNewSessionData -> {
state.copy(newSessionData = action.newSession)
}
is WalletConnectAction.SetSessionsRestored ->
WalletConnectState(sessions = action.sessions)
is WalletConnectAction.SetSessionsRestored -> state.copy(
sessions = action.sessions,
)
is WalletConnectAction.RemoveSession -> {
val sessions =
state.sessions.filterNot { it.session.toUri() == action.session.toUri() }

View file

@ -9,15 +9,16 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.features.signin.redux.SignInAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
@ -112,14 +113,32 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet) }
sendSignedInCardAnalyticsEvent(scanResponse)
scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
}
.doOnResult {
store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card))
navigateTo(AppScreen.Wallet)
}
}
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
val currency = ParamCardCurrencyConverter().convert(
value = scanResponse.cardTypesResolver,
)
if (currency != null) {
Analytics.send(
event = Basic.SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
signInType = Basic.SignedIn.SignInType.Card,
walletsCount = userWalletsListManager.walletsCount.toString(),
hasBackup = scanResponse.card.backupStatus?.isActive,
),
)
}
}
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
delay(HIDE_PROGRESS_DELAY)

View file

@ -80,16 +80,20 @@ object OnboardingHelper {
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchOnMain(SaveWalletAction.Save)
val toggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
if (toggles.isGeneralManagerEnabled) {
store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError))
} else {
store.dispatchWithMain(SaveWalletAction.Save)
}
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen

View file

@ -11,7 +11,7 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.isLockedSync
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
@ -356,7 +356,9 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
private fun getPopBackScreen(): AppScreen {
return if (userWalletsListManager.hasUserWallets) {
if (userWalletsListManager.isLockedSync) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
if (isLocked) {
AppScreen.Welcome
} else {
AppScreen.Wallet

View file

@ -27,4 +27,6 @@ internal sealed interface SaveWalletAction : Action {
}
data object SaveWalletWasShown : SaveWalletAction
data class SaveWalletAfterBackup(val hasBackupError: Boolean) : SaveWalletAction
}

View file

@ -1,17 +1,15 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.CompletionResult
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.userwallets.UserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.isLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
@ -19,15 +17,22 @@ import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import org.rekotlin.Store
import timber.log.Timber
internal class SaveWalletMiddleware {
private val saveWalletJobHolder = JobHolder()
val middleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
@ -47,6 +52,7 @@ internal class SaveWalletMiddleware {
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown()
is SaveWalletAction.Dismiss -> dismiss(state)
is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(state, action.hasBackupError)
is SaveWalletAction.Save.Success,
is SaveWalletAction.ProvideBackupInfo,
is SaveWalletAction.CloseError,
@ -57,6 +63,31 @@ internal class SaveWalletMiddleware {
}
}
private fun saveWalletAfterBackup(state: SaveWalletState, hasBackupError: Boolean) {
scope.launch {
val backupInfo = state.backupInfo ?: error("Backup info is null")
val userWallet = UserWalletBuilder(backupInfo.scanResponse)
.backupCardsIds(state.backupInfo.backupCardsIds)
.hasBackupError(hasBackupError)
.build()
.guard {
Timber.e("User wallet not created")
return@launch
}
userWalletsListManager.save(userWallet, canOverride = true)
.flatMap {
saveAccessCodeIfNeeded(accessCode = backupInfo.accessCode, cardsInWallet = userWallet.cardsInWallet)
}
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess { mainScope.launch { store.onUserWalletSelected(userWallet) } }
.doOnResult { store.navigateToWallet() }
}
}
private fun enrollBiometrics() {
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
}
@ -99,9 +130,7 @@ internal class SaveWalletMiddleware {
?: return@launch
val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
if (featureToggles.isGeneralManagerEnabled) {
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
} else {
if (!featureToggles.isGeneralManagerEnabled) {
provideLockableUserWalletsListManagerIfNot()
}
@ -134,16 +163,9 @@ internal class SaveWalletMiddleware {
}
store.dispatchOnMain(SaveWalletAction.Save.Success)
store.dispatchOnMain(
if (store.state.navigationState.backStack.contains(AppScreen.Wallet)) {
NavigationAction.PopBackTo(AppScreen.Wallet)
} else {
NavigationAction.NavigateTo(AppScreen.Wallet)
},
)
store.navigateToWallet()
}
}
}.saveIn(saveWalletJobHolder)
}
private fun allowToUseBiometrics(state: SaveWalletState) {
@ -152,10 +174,6 @@ internal class SaveWalletMiddleware {
return
}
val scanResponse = state.backupInfo?.scanResponse
?: store.state.globalState.scanResponse
?: return
if (state.backupInfo != null) {
// TODO: Remove after onboarding refactoring
Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On))
@ -164,31 +182,30 @@ internal class SaveWalletMiddleware {
}
scope.launch {
val userWallet = userWalletsListManager.selectedUserWalletSync
?: UserWalletBuilder(scanResponse)
.backupCardsIds(state.backupInfo?.backupCardsIds)
.build()
?: return@launch
/*
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
* because it will be automatically saved on UserWalletsListManager switch
*/
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
val error = IllegalStateException("No selected user wallet")
Timber.e(error, "Unable to save user wallet")
store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error)))
return@launch
}
saveAccessCodeIfNeeded(accessCode = state.backupInfo?.accessCode, cardsInWallet = userWallet.cardsInWallet)
.flatMap {
userWalletsListManager.save(userWallet, canOverride = true)
}
.doOnFailure { error ->
store.dispatchWithMain(SaveWalletAction.Save.Error(error))
}
.doOnSuccess {
preferencesStorage.shouldSaveAccessCodes = true
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
handleSuccessAllowing(selectedUserWallet)
}.saveIn(saveWalletJobHolder)
}
store.dispatchOnMain(SaveWalletAction.Save.Success)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
}
}
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
preferencesStorage.shouldSaveAccessCodes = true
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = userWallet.hasAccessCode,
)
store.dispatchWithMain(SaveWalletAction.Save.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
}
private suspend fun provideLockableUserWalletsListManagerIfNot() {
@ -223,15 +240,25 @@ internal class SaveWalletMiddleware {
cardsInWallet: Set<String>,
): CompletionResult<Unit> {
return when {
accessCode != null -> {
accessCode.isNullOrBlank() -> {
CompletionResult.Success(Unit)
}
else -> {
tangemSdkManager.saveAccessCode(
accessCode = accessCode,
cardsIds = cardsInWallet,
)
}
else -> {
CompletionResult.Success(Unit)
}
}
}
private suspend fun Store<AppState>.navigateToWallet() {
dispatchWithMain(
if (store.state.navigationState.backStack.contains(AppScreen.Wallet)) {
NavigationAction.PopBackTo(AppScreen.Wallet)
} else {
NavigationAction.NavigateTo(AppScreen.Wallet)
},
)
}
}

View file

@ -47,7 +47,9 @@ internal object SaveWalletReducer {
needEnrollBiometrics = false,
isSaveInProgress = false,
)
is SaveWalletAction.SaveWalletWasShown -> state
is SaveWalletAction.SaveWalletWasShown,
is SaveWalletAction.SaveWalletAfterBackup,
-> state
}
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.features.signin.redux
import com.tangem.core.analytics.models.Basic
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed interface SignInAction : Action {
data class SetSignInType(val type: Basic.SignedIn.SignInType) : SignInAction
}

View file

@ -1,15 +0,0 @@
package com.tangem.tap.features.signin.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Middleware
/**
[REDACTED_AUTHOR]
*/
object SignInMiddleware {
val middleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action -> next(action) }
}
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.features.signin.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
object SignInReducer {
fun reduce(action: Action, state: AppState): SignInState {
if (action !is SignInAction) return state.signInState
return when (action) {
is SignInAction.SetSignInType -> state.signInState.copy(type = action.type)
}
}
}

View file

@ -1,8 +0,0 @@
package com.tangem.tap.features.signin.redux
import com.tangem.core.analytics.models.Basic
/**
[REDACTED_AUTHOR]
*/
data class SignInState(val type: Basic.SignedIn.SignInType? = null)

View file

@ -25,7 +25,6 @@ import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.signin.redux.SignInAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -98,7 +97,6 @@ internal class WelcomeMiddleware {
signInType = Basic.SignedIn.SignInType.Biometric,
)
store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric))
store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(userWallet = selectedUserWallet)
@ -128,7 +126,6 @@ internal class WelcomeMiddleware {
.doOnSuccess {
sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card)
store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card))
store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet = userWallet)

View file

@ -0,0 +1,11 @@
package com.tangem.tap.network.auth
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.wallet.BuildConfig
internal class DefaultAppVersionProvider : AppVersionProvider {
override fun getAppVersion(): String {
return BuildConfig.VERSION_NAME
}
}

View file

@ -2,8 +2,10 @@ package com.tangem.tap.network.auth.di
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.tap.network.auth.DefaultAppVersionProvider
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.proxy.AppStateHolder
@ -34,4 +36,10 @@ class AuthModule {
configManager = configManager,
)
}
@Provides
@Singleton
fun provideAppVersionProvider(): AppVersionProvider {
return DefaultAppVersionProvider()
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.ExpressAuthProvider
import dagger.Module
import dagger.Provides
@ -30,6 +31,7 @@ class NetworkModule {
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
): TangemExpressApi {
val url = if (BuildConfig.ENVIRONMENT == "dev") {
DEV_EXPRESS_BASE_URL
@ -43,6 +45,7 @@ class NetworkModule {
.client(
OkHttpClient.Builder()
.addHeaders(Express(expressAuthProvider))
.addHeaders(AppVersionPlatformHeaders(appVersionProvider))
.addLoggers(context)
.build(),
)

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.utils
import com.tangem.lib.auth.AppVersionProvider
import com.tangem.lib.auth.AuthBearerProvider
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
@ -30,4 +31,9 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
class AuthBearerHeader(authBearerProvider: AuthBearerProvider) : RequestHeader(
"Authorization" to { "Bearer " + authBearerProvider.getApiKey() },
)
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
"version" to { appVersionProvider.getAppVersion() },
"platform" to { "android" },
)
}

View file

@ -34,7 +34,9 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
*/
@Composable
fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) {
Row(
var rootWidth by remember { mutableIntStateOf(value = 0) }
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
@ -42,27 +44,13 @@ fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size72)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
.padding(all = TangemTheme.dimens.spacing12)
.onSizeChanged { rootWidth = it.width },
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
horizontalAlignment = Alignment.Start,
) {
var rootWidth by remember { mutableIntStateOf(value = 0) }
Column(
modifier = Modifier.onSizeChanged { rootWidth = it.width },
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
horizontalAlignment = Alignment.Start,
) {
Title(currencyName = state.currencySymbol)
Content(state = state, rootWidth = rootWidth)
}
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
Title(currencyName = state.currencySymbol)
Content(state = state, rootWidth = rootWidth)
}
}

View file

@ -478,11 +478,11 @@ internal class StateBuilder(
fromToken: CryptoCurrency,
warnings: MutableList<SwapWarning>,
) {
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState
if (feeEnoughState is SwapFeeState.NotEnough &&
quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.permissionState !is PermissionDataState.PermissionLoading
) {
val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return
val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough &&
quoteModel.permissionState !is PermissionDataState.PermissionLoading &&
feeEnoughState.feeCurrency != fromToken
if (needShowCoverWarning) {
warnings.add(
SwapWarning.UnableToCoverFeeWarning(
createUnableToCoverFeeNotificationConfig(
@ -1144,7 +1144,6 @@ internal class StateBuilder(
fun showSelectProviderBottomSheet(
uiState: SwapStateHolder,
selectedProviderId: String,
bestRatedProviderId: String,
pricesLowerBest: Map<String, Float>,
providersStates: Map<SwapProvider, SwapState>,
unavailableProviders: List<SwapProvider>,
@ -1152,7 +1151,7 @@ internal class StateBuilder(
): SwapStateHolder {
val availableProvidersStates = providersStates.entries
.mapNotNull {
it.convertToProviderBottomSheetState(pricesLowerBest, bestRatedProviderId, actions.onProviderSelect)
it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect)
}
.sortedWith(ProviderPercentDiffComparator)
val unavailableProviderStates = unavailableProviders.map {
@ -1300,7 +1299,6 @@ internal class StateBuilder(
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderBottomSheetState(
pricesLowerBest: Map<String, Float>,
bestRatedProviderId: String,
onProviderSelect: (String) -> Unit,
): ProviderState? {
val provider = this.key
@ -1308,7 +1306,6 @@ internal class StateBuilder(
is SwapState.EmptyAmountState -> null
is SwapState.QuotesLoadedState -> {
provider.convertToContentSelectableProviderState(
isBestRate = bestRatedProviderId == provider.providerId,
state = state,
onProviderClick = onProviderSelect,
pricesLowerBest = pricesLowerBest,
@ -1447,7 +1444,6 @@ internal class StateBuilder(
}
private fun SwapProvider.convertToContentSelectableProviderState(
isBestRate: Boolean,
state: SwapState.QuotesLoadedState,
selectionType: ProviderState.SelectionType,
pricesLowerBest: Map<String, Float>,
@ -1457,8 +1453,6 @@ internal class StateBuilder(
val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency)
val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) {
ProviderState.AdditionalBadge.PermissionRequired
} else if (isBestRate) {
ProviderState.AdditionalBadge.BestTrade
} else {
ProviderState.AdditionalBadge.Empty
}

View file

@ -940,7 +940,6 @@ internal class SwapViewModel @Inject constructor(
selectedProviderId = providerId,
pricesLowerBest = pricesLowerBest,
unavailableProviders = unavailableProviders,
bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId,
providersStates = dataState.lastLoadedSwapStates,
) { uiState = stateBuilder.dismissBottomSheet(uiState) }
},

View file

@ -1,16 +1,24 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* Reinitialize wallet transformer
*
* @property prevWalletId reinitialized wallet id
* @property newUserWallet new user wallet
* @property clickIntents click intents
*
[REDACTED_AUTHOR]
*/
internal class ReinitializeWalletTransformer(
private val userWallet: UserWallet,
private val prevWalletId: UserWalletId,
private val newUserWallet: UserWallet,
private val clickIntents: WalletClickIntents,
) : WalletScreenStateTransformer {
@ -18,9 +26,10 @@ internal class ReinitializeWalletTransformer(
override fun transform(prevState: WalletScreenState): WalletScreenState {
return prevState.copy(
wallets = persistentListOf(
walletLoadingStateFactory.create(userWallet),
),
wallets = prevState.wallets
.filterNot { it.walletCardState.id == prevWalletId }
.plus(element = walletLoadingStateFactory.create(newUserWallet))
.toImmutableList(),
)
}
}

View file

@ -223,7 +223,11 @@ internal class WalletViewModel @Inject constructor(
)
stateHolder.update(
ReinitializeWalletTransformer(userWallet = action.selectedWallet, clickIntents = clickIntents),
ReinitializeWalletTransformer(
prevWalletId = action.prevWalletId,
newUserWallet = action.selectedWallet,
clickIntents = clickIntents,
),
)
}

View file

@ -214,7 +214,10 @@ internal class WalletsUpdateActionResolver @Inject constructor(
}
/**
* Reinitialize selected wallet. Example, scanning a new card if wallets saving is turned off
* Reinitialize selected wallet.
* Uses when:
* 1. user scanned a new card but wallets saving is turned off;
* 2. user reset Twins.
*
* @property prevWalletId previous selected wallet id
* @property selectedWallet selected wallet

View file

@ -80,11 +80,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onAddBackupCardClick() {
analyticsEventHandler.send(MainScreen.NoticeBackupYourWalletTapped)
prepareOnboardingProcess()
router.openOnboardingScreen()
prepareAndStartOnboardingProcess()
}
private fun prepareOnboardingProcess() {
private fun prepareAndStartOnboardingProcess() {
viewModelScope.launch(dispatchers.main) {
getSelectedUserWallet()?.let {
reduxStateHolder.dispatch(
@ -94,6 +93,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
),
)
}
// navigation action shouldn't be out of coroutine to avoid race
router.openOnboardingScreen()
}
}

View file

@ -85,9 +85,9 @@ leakcanary = "2.13"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-546"
tangemBlockchainSdk = "develop-547"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-337"
tangemCardSdk = "develop-339"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem

View file

@ -0,0 +1,6 @@
package com.tangem.lib.auth
interface AppVersionProvider {
fun getAppVersion(): String
}