Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-23 18:21:55 +03:00
commit c00aed90d8
1063 changed files with 33256 additions and 13316 deletions

View file

@ -14,7 +14,8 @@ import androidx.lifecycle.Lifecycle
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRouter
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -36,7 +37,7 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP
}
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.customtoken.impl.presentation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -8,7 +9,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen
@ -32,15 +32,12 @@ internal class AddCustomTokenFragment : ComposeFragment() {
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(this)
}
val statusBarColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(color = statusBarColor)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
AddCustomTokenScreen(
modifier = Modifier.systemBarsPadding(),
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.systemBarsPadding(),
stateHolder = state,
)
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.customtoken.impl.presentation.routers
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.popTo
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -12,11 +14,11 @@ import com.tangem.wallet.R
internal class DefaultCustomTokenRouter : CustomTokenRouter {
override fun popBackStack() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
}
override fun openWalletScreen() {
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
override fun openUnsupportedNetworkAlert(blockchain: Blockchain) {
@ -32,7 +34,7 @@ internal class DefaultCustomTokenRouter : CustomTokenRouter {
val alert = AppDialog.SimpleOkDialogRes(
headerId = R.string.common_error,
messageId = R.string.common_unknown_error,
onOk = { store.dispatch(NavigationAction.PopBackTo()) },
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
)
store.dispatchDialogShow(alert)
}

View file

@ -4,7 +4,6 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
@ -21,7 +20,6 @@ object DemoHelper {
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
BackupAction.StartBackup::class.java,
DetailsAction.ResetToFactory.Start::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)

View file

@ -14,26 +14,6 @@ sealed class DetailsAction : Action {
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
data object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
data object Start : ResetToFactory()
data object Proceed : ResetToFactory()
data class AcceptCondition1(val accepted: Boolean) : ResetToFactory()
data class AcceptCondition2(val accepted: Boolean) : ResetToFactory()
data object Failure : ResetToFactory()
data object Success : ResetToFactory()
data class ShowDialog(val dialog: CardSettingsState.Dialog) : ResetToFactory()
data object DismissDialog : ResetToFactory()
}
data object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction()
data object ResetCardSettingsData : DetailsAction()
data object ScanAndSaveUserWallet : DetailsAction() {
data object Success : DetailsAction()
@ -43,26 +23,6 @@ sealed class DetailsAction : Action {
data object DismissError : DetailsAction()
sealed class AccessCodeRecovery : DetailsAction() {
object Open : AccessCodeRecovery()
data class SaveChanges(val enabled: Boolean) : AccessCodeRecovery() {
data class Success(val enabled: Boolean) : AccessCodeRecovery()
}
data class SelectOption(val enabled: Boolean) : AccessCodeRecovery()
}
sealed class ManageSecurity : DetailsAction() {
data object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
data object SaveChanges : ManageSecurity() {
data object Success : ManageSecurity()
data object Failure : ManageSecurity()
}
data object ChangeAccessCode : ManageSecurity()
}
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,
@ -96,6 +56,8 @@ sealed class DetailsAction : Action {
data class ChangeAppCurrency(
val currency: AppCurrency,
) : AppSettings()
data class Prepare(val state: AppSettingsState) : AppSettings()
}
data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction()

View file

@ -1,40 +1,36 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.*
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.asLockable
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.Settings
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.extensions.dispatchNavigationAction
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.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@ -42,17 +38,13 @@ 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
import org.rekotlin.Middleware
import timber.log.Timber
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val appSettingsMiddleware = AppSettingsMiddleware()
private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
@ -69,152 +61,11 @@ class DetailsMiddleware {
private fun handleAction(state: DetailsState, action: Action) {
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state)
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
is DetailsAction.ReCreateTwinsWallet -> {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action)
is DetailsAction.ScanCard -> scanCard(state)
is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet()
}
}
class EraseWalletMiddleware {
@Suppress("CyclomaticComplexMethod")
fun handle(action: DetailsAction.ResetToFactory) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
when (action) {
is DetailsAction.ResetToFactory.Start -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins) {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
}
}
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
val userWalletId = UserWalletIdBuilder.card(card).build()
// we must require a password regardless of biometric settings
val policy = tangemSdkManager.userCodeRequestPolicy
val doBeforeErase = {
val type = if (card.isAccessCodeSet) {
UserCodeType.AccessCode
} else if (card.isPasscodeSet == true) {
UserCodeType.Passcode
} else {
null
}
type?.let {
tangemSdkManager.setUserCodeRequestPolicy(UserCodeRequestPolicy.Always(type))
}
}
val doAfterErase = {
tangemSdkManager.setUserCodeRequestPolicy(policy)
}
doBeforeErase()
tangemSdkManager.resetToFactorySettings(card.cardId, true)
.flatMap {
userWalletsListManager.delete(listOfNotNull(userWalletId))
}
.flatMap {
tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId))
}
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
} else {
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))
}
}
}
.doOnFailure { error ->
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error))
}
}
.doOnResult {
doAfterErase()
}
}
}
else -> Unit
}
}
}
class ManageSecurityMiddleware {
@Suppress("ComplexMethod")
fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) {
when (action) {
is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
}
is DetailsAction.ManageSecurity.SaveChanges -> {
val cardSettingsState = detailsState.cardSettingsState
val cardId = cardSettingsState?.card?.cardId
val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption
scope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
else -> return@launch
}
withContext(Dispatchers.Main) {
val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
when (result) {
is CompletionResult.Success -> {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption))
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
}
else -> Unit
}
}
}
}
is DetailsAction.ManageSecurity.ChangeAccessCode -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
when (tangemSdkManager.setAccessCode(card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {}
}
}
}
else -> Unit
}
}
}
class AppSettingsMiddleware {
private val checkBiometricsStatusJobHolder = JobHolder()
@ -246,6 +97,7 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
is DetailsAction.AppSettings.Prepare,
-> Unit
}
}
@ -274,7 +126,7 @@ class DetailsMiddleware {
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
activityResultCaller.openSystemBiometrySettings()
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
@ -395,7 +247,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
return CompletionResult.Success(Unit)
}
@ -429,63 +281,6 @@ class DetailsMiddleware {
}
}
class AccessCodeRecoveryMiddleware {
fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) {
when (action) {
is DetailsAction.AccessCodeRecovery.Open -> {
Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges -> {
scope.launch {
tangemSdkManager
.setAccessCodeRecoveryEnabled(state.cardSettingsState?.card?.cardId, action.enabled)
.doOnSuccess {
Analytics.send(
Settings.CardSettings.AccessCodeRecoveryChanged(
AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled),
),
)
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchOnMain(
DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled),
)
}
}
}
is DetailsAction.AccessCodeRecovery.SelectOption -> Unit
is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> Unit
}
}
}
private fun scanCard(state: DetailsState) = scope.launch {
store.inject(DaggerGraphState::scanCardProcessor)
.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
val currentUserWalletId = state.scanResponse
?.let { UserWalletIdBuilder.scanResponse(it).build() }
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
.build()
val isSameWallet = currentUserWalletId == scannedUserWalletId
if (isSameWallet) {
store.dispatchOnMain(
DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse),
)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
}
}
}
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
@ -502,7 +297,7 @@ class DetailsMiddleware {
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
},
disclaimerWillShow = {
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
onSuccess = { scanResponse ->
createUserWallet(scanResponse)
@ -546,7 +341,7 @@ class DetailsMiddleware {
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
.doOnFailure { error ->
if (error is UserWalletsListError.WalletAlreadySaved) {
@ -554,7 +349,7 @@ class DetailsMiddleware {
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
} else {
Timber.e(error, "Unable to create user wallet")
handleError(error, prevUseBiometricsForAccessCode)

View file

@ -1,21 +1,15 @@
package com.tangem.tap.features.details.redux
import com.tangem.core.navigation.AppScreen
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import java.util.EnumSet
object DetailsReducer {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
@ -29,16 +23,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, state)
}
is DetailsAction.PrepareCardSettingsData -> {
handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState)
}
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
@ -47,7 +31,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
selectedAppCurrency = action.currency,
),
)
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy(
isScanningInProgress = true,
)
@ -61,19 +44,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.DismissError -> detailsState.copy(
error = null,
)
else -> detailsState
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState {
return DetailsState(
scanResponse = action.scanResponse,
// If the current screen is ResetToFactory, we must save the ResetToFactory's state
cardSettingsState = if (store.state.navigationState.backStack.lastOrNull() == AppScreen.ResetToFactory) {
state.detailsState.cardSettingsState
} else {
null
},
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appSettingsState = AppSettingsState(
isBiometricsAvailable = runBlocking {
@ -96,139 +72,6 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
)
}
private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState {
val cardTypesResolver = scanResponse.cardTypesResolver
val card = scanResponse.card
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
val cardSettingsState = CardSettingsState(
cardInfo = card.toCardInfo(cardTypesResolver),
scanResponse = scanResponse,
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
card = scanResponse.card,
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
resetButtonEnabled = false,
condition1Checked = false,
condition2Checked = false,
accessCodeRecovery = if (cardTypesResolver.isWallet2()) {
val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false
AccessCodeRecoveryState(
enabledOnCard = enabled,
enabledSelection = enabled,
)
} else {
null
},
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
dialog = null,
)
return state.copy(cardSettingsState = cardSettingsState)
}
private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesResolver): ManageSecurityState {
val securityOption = when {
card.isAccessCodeSet -> {
SecurityOption.AccessCode
}
card.isPasscodeSet == true -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
val allowedSecurityOptions = when {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> {
EnumSet.of(SecurityOption.LongTap)
}
card.settings.isBackupAllowed -> {
EnumSet.of(securityOption)
}
else ->
prepareAllowedSecurityOptions(cardTypesResolver = cardTypesResolver, currentSecurityOption = securityOption)
}
return ManageSecurityState(
currentOption = securityOption,
allowedOptions = allowedSecurityOptions,
selectedOption = securityOption,
)
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}
private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState {
val cardSettingsState = state.cardSettingsState
return when (action) {
is DetailsAction.ResetToFactory.AcceptCondition1 -> {
val warning1Checked = action.accepted
val resetButtonEnabled = if (cardSettingsState?.isShowPasswordResetRadioButton == true) {
warning1Checked && cardSettingsState.condition2Checked
} else {
warning1Checked
}
state.copy(
cardSettingsState = cardSettingsState?.copy(
condition1Checked = action.accepted,
resetButtonEnabled = resetButtonEnabled,
),
)
}
is DetailsAction.ResetToFactory.AcceptCondition2 -> {
val warning2Checked = action.accepted
state.copy(
cardSettingsState = cardSettingsState?.copy(
condition2Checked = action.accepted,
resetButtonEnabled = warning2Checked && cardSettingsState.condition1Checked,
),
)
}
is DetailsAction.ResetToFactory.ShowDialog -> {
state.copy(cardSettingsState = cardSettingsState?.copy(dialog = action.dialog))
}
is DetailsAction.ResetToFactory.DismissDialog -> {
state.copy(cardSettingsState = cardSettingsState?.copy(dialog = null))
}
else -> state
}
}
private fun handleSecurityAction(action: DetailsAction.ManageSecurity, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SelectOption -> {
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
selectedOption = action.option,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState))
}
is DetailsAction.ManageSecurity.SaveChanges.Success -> {
// Setting options to show only LongTap from now on for non-twins
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
currentOption = state.cardSettingsState.manageSecurityState.selectedOption,
allowedOptions = state.scanResponse?.cardTypesResolver?.let {
prepareAllowedSecurityOptions(
cardTypesResolver = it,
currentSecurityOption = state.cardSettingsState.manageSecurityState.selectedOption,
)
} ?: EnumSet.of(SecurityOption.LongTap),
)
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
manageSecurityState = manageSecurityState,
),
)
}
else -> state
}
}
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@ -281,69 +124,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
isHidingEnabled = action.hideBalance,
),
)
is DetailsAction.AppSettings.Prepare -> state.copy(
appSettingsState = action.state,
)
is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state
}
}
private fun handleAccessCodeRecoveryAction(
action: DetailsAction.AccessCodeRecovery,
state: DetailsState,
): DetailsState {
return when (action) {
DetailsAction.AccessCodeRecovery.Open -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledSelection = state.cardSettingsState.accessCodeRecovery.enabledOnCard,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges -> state
is DetailsAction.AccessCodeRecovery.SelectOption -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledSelection = action.enabled,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledOnCard = action.enabled,
enabledSelection = action.enabled,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
}
}
private fun prepareAllowedSecurityOptions(
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
if (cardTypesResolver.isTangemTwins()) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (currentSecurityOption == SecurityOption.PassCode) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
return allowedSecurityOptions
}
@Suppress("MagicNumber")
private fun CardDTO.toCardInfo(cardTypesResolver: CardTypesResolver): CardInfo {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.issuer.name
val signedHashes = this.signedHashesCount()
return CardInfo(
cardId = cardId,
issuer = issuer,
signedHashes = signedHashes,
isTwin = cardTypesResolver.isTangemTwins(),
hasBackup = backupStatus?.isActive == true,
)
}

View file

@ -3,15 +3,11 @@ package com.tangem.tap.features.details.redux
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.Button
import org.rekotlin.StateType
import java.util.EnumSet
data class DetailsState(
val scanResponse: ScanResponse? = null,
val cardSettingsState: CardSettingsState? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val isScanningInProgress: Boolean = false,
@ -19,52 +15,13 @@ data class DetailsState(
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
data class CardInfo(
val cardId: String,
val issuer: String,
val signedHashes: Int,
val isTwin: Boolean,
val hasBackup: Boolean,
)
/**
* @property enabledOnCard whether access code recovery is enabled on card
* @property enabledSelection current selected option in app (not saved on card yet)
*/
data class AccessCodeRecoveryState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
)
data class CardSettingsState(
val cardInfo: CardInfo,
val card: CardDTO,
val scanResponse: ScanResponse,
val manageSecurityState: ManageSecurityState?,
val resetCardAllowed: Boolean,
val resetButtonEnabled: Boolean,
val condition1Checked: Boolean,
val condition2Checked: Boolean,
val accessCodeRecovery: AccessCodeRecoveryState? = null,
val isShowPasswordResetRadioButton: Boolean,
val dialog: Dialog?,
) {
sealed interface Dialog {
data object StartResetDialog : Dialog
data object ContinueResetDialog : Dialog
data object InterruptedResetDialog : Dialog
data object CompletedResetDialog : Dialog
}
sealed class ResetCardDialog {
data object StartResetDialog : ResetCardDialog()
data object ContinueResetDialog : ResetCardDialog()
data object InterruptedResetDialog : ResetCardDialog()
data object CompletedResetDialog : ResetCardDialog()
}
data class ManageSecurityState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true),
)
data class AppSettingsState(
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,

View file

@ -1,19 +1,17 @@
package com.tangem.tap.features.details.redux.walletconnect
import androidx.core.os.bundleOf
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.Account
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
@ -65,14 +63,7 @@ class WalletConnectMiddleware {
// TODO check
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(
NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT,
),
),
)
store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) }
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {

View file

@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.navigation.StateDialog
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents

View file

@ -6,8 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -22,12 +21,8 @@ internal class AppCurrencySelectorFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
val systemBarsColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
NavigationBar3ButtonsScrim()
AppCurrencySelectorScreen(
modifier = modifier,
state = uiState,

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -40,7 +41,9 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier
val listState = rememberLazyListState()
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
modifier = modifier
.imePadding()
.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = TangemTheme.colors.background.secondary,
topBar = {
TopBar(
@ -49,6 +52,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier
state = state,
)
},
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
content = { paddingValues ->
val contentModifier = Modifier
.padding(paddingValues)
@ -62,7 +66,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier
modifier = contentModifier,
listState = listState,
currencies = state.items,
selectedId = state.selectedId.orEmpty(),
selectedId = state.selectedId,
onCurrencyClick = state.onCurrencyClick,
)
}
@ -175,6 +179,7 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi
@Composable
private fun LoadingList(modifier: Modifier = Modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(modifier = modifier) {
repeat(times = 10) {
Row(
@ -199,6 +204,7 @@ private fun LoadingList(modifier: Modifier = Modifier) {
)
}
}
Spacer(Modifier.height(bottomBarHeight))
}
}
@ -210,9 +216,11 @@ private fun CurrenciesList(
onCurrencyClick: (Currency) -> Unit,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = modifier,
state = listState,
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
items(
items = currencies,

View file

@ -2,8 +2,9 @@ package com.tangem.tap.features.details.ui.appcurrency
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
@ -20,7 +21,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase,
private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase,
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), AppCurrencySelectorIntents {
@ -34,7 +35,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
val uiState: StateFlow<AppCurrencySelectorState> = stateController.stateFlow
override fun onBackClick() {
reduxNavController.popBackStack()
router.pop()
}
override fun onSearchClick() {
@ -52,7 +53,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
analyticsEventHandler.send(
event = Settings.AppSettings.MainCurrencyChanged(currencyType = currency.name),
)
reduxNavController.popBackStack()
router.pop()
}
}
}

View file

@ -6,11 +6,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -35,8 +35,7 @@ internal class AppSettingsFragment : ComposeFragment() {
modifier = modifier,
state = state,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
)
}

View file

@ -1,13 +1,17 @@
package com.tangem.tap.features.details.ui.appsettings
import android.content.res.Configuration
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -32,6 +36,7 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () ->
},
titleRes = R.string.app_settings_title,
onBackClick = onBackClick,
addBottomInsets = false,
)
}
@ -44,7 +49,11 @@ private fun AppSettings(state: AppSettingsScreenState.Content) {
null -> Unit
}
LazyColumn {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
contentPadding = PaddingValues(bottom = bottomBarHeight),
) {
items(
items = state.items,
key = Item::id,

View file

@ -1,16 +1,20 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.*
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.features.details.redux.AppSetting
@ -26,14 +30,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class AppSettingsViewModel @Inject constructor(
private val appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val balanceHidingRepository: BalanceHidingRepository,
private val analyticsEventHandler: AnalyticsEventHandler,
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
private val detailsFeatureToggles: DetailsFeatureToggles,
) : ViewModel(),
StoreSubscriber<DetailsState>,
DefaultLifecycleObserver {
@ -50,6 +62,9 @@ internal class AppSettingsViewModel @Inject constructor(
init {
bootstrapAppCurrencyUpdates()
if (detailsFeatureToggles.isRedesignEnabled) {
bootstrapBiometricsUpdates()
}
subscribeToStoreChanges()
sendItemsAnalytics()
@ -128,7 +143,7 @@ internal class AppSettingsViewModel @Inject constructor(
}
private fun showAppCurrencySelector() {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppCurrencySelector))
store.dispatchNavigationAction { push(AppRoute.AppCurrencySelector) }
}
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
@ -214,6 +229,19 @@ internal class AppSettingsViewModel @Inject constructor(
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun bootstrapBiometricsUpdates() = viewModelScope.launch {
val state = AppSettingsState(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),
isBiometricsAvailable = canUseBiometryUseCase(),
isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings,
selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default,
selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT,
)
store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state))
}
private fun subscribeToStoreChanges() {
store.subscribe(subscriber = this) { state ->
state.skipRepeats { oldState, newState ->

View file

@ -1,13 +1,14 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.fragment.app.viewModels
import com.tangem.core.navigation.NavigationAction
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -22,14 +23,13 @@ internal class CardSettingsFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
val state by viewModel.screenState.collectAsStateWithLifecycle()
CardSettingsScreen(
modifier = modifier,
state = viewModel.screenState.value,
state = state,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
)
}

View file

@ -4,16 +4,12 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import com.tangem.wallet.R
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
internal data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null,
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
)
@ -44,7 +40,7 @@ internal sealed class CardInfo(
clickable = clickable,
)
object ChangeAccessCode : CardInfo(
data object ChangeAccessCode : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_change_access_code),
subtitle = TextReference.Res(R.string.card_settings_change_access_code_footer),
clickable = true,
@ -60,9 +56,9 @@ internal sealed class CardInfo(
clickable = true,
)
class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo(
class ResetToFactorySettings(description: TextReference) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
subtitle = cardInfo.toResetCardDescriptionText(),
subtitle = description,
clickable = true,
)
}

View file

@ -1,105 +1,124 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import arrow.core.Either
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.ui.common.utils.*
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import org.rekotlin.StoreSubscriber
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
internal class CardSettingsViewModel @Inject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) :
ViewModel(), DefaultLifecycleObserver, StoreSubscriber<DetailsState> {
private val scanCardProcessor: ScanCardProcessor,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tangemSdkManager: TangemSdkManager,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
var screenState: MutableState<CardSettingsScreenState> =
mutableStateOf(updateState(store.state.detailsState.cardSettingsState))
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.CardSettings.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for CardSettingsViewModel")
override fun onStart(owner: LifecycleOwner) {
when (val selectedWalletEither = getSelectedWalletSyncUseCase()) {
is Either.Left -> {
Timber.e(selectedWalletEither.value.toString())
val screenState: MutableStateFlow<CardSettingsScreenState> = MutableStateFlow(getInitialState())
private fun getInitialState() = CardSettingsScreenState(
cardDetails = null,
onElementClick = ::handleClickingItem,
onScanCardClick = ::scanCard,
)
private fun scanCard() = viewModelScope.launch {
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val isCorrectUserWalletScanned = scannedUserWalletId == userWalletId
if (isCorrectUserWalletScanned) {
val userWallet = getUserWallet()
updateCardDetails(userWallet)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
}
}
}
private fun updateCardDetails(userWallet: UserWallet) {
val card = userWallet.scanResponse.card
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
val cardId = cardTypesResolver.getCardId()
val currentSecurityOption = getCurrentSecurityOption(card)
val allowedSecurityOptions = getAllowedSecurityOptions(
card = card,
cardTypesResolver = cardTypesResolver,
currentSecurityOption = currentSecurityOption,
)
val isResetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver)
val cardDetails = buildList {
CardInfo.CardId(cardId).let(::add)
CardInfo.Issuer(card.issuer.name).let(::add)
if (!cardTypesResolver.isTangemTwins()) {
CardInfo.SignedHashes(card.signedHashesCount().toString()).let(::add)
}
CardInfo.SecurityMode(
currentSecurityOption,
clickable = allowedSecurityOptions.size > 1,
).let(::add)
if (card.backupStatus?.isActive == true && card.isAccessCodeSet) {
CardInfo.ChangeAccessCode.let(::add)
}
if (isAccessCodeRecoveryAllowed(cardTypesResolver)) {
CardInfo.AccessCodeRecovery(isAccessCodeRecoveryEnabled(cardTypesResolver, card)).let(::add)
}
if (isResetCardAllowed) {
CardInfo.ResetToFactorySettings(
description = getResetToFactoryDescription(card, cardTypesResolver),
).let(::add)
}
is Either.Right -> Unit
}
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop(owner: LifecycleOwner) {
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
screenState.value = updateState(state.cardSettingsState)
}
private fun updateState(state: CardSettingsState?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) {
CardSettingsScreenState(
cardDetails = null,
accessCodeRecoveryState = null,
onElementClick = {},
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
},
)
} else {
val cardId = if (state.card.isTangemTwins) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
val cardDetails: MutableList<CardInfo> = mutableListOf(
CardInfo.CardId(cardId),
CardInfo.Issuer(state.cardInfo.issuer),
)
if (!state.card.isTangemTwins) {
cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
}
cardDetails.add(
CardInfo.SecurityMode(
securityOption = state.manageSecurityState.currentOption,
clickable = state.manageSecurityState.allowedOptions.size > 1,
),
)
if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
cardDetails.add(CardInfo.ChangeAccessCode)
}
if (state.accessCodeRecovery != null) {
cardDetails.add(CardInfo.AccessCodeRecovery(state.accessCodeRecovery.enabledOnCard))
}
if (state.resetCardAllowed) {
cardDetails.add(CardInfo.ResetToFactorySettings(state.cardInfo))
}
CardSettingsScreenState(
cardDetails = cardDetails,
accessCodeRecoveryState = state.accessCodeRecovery,
onScanCardClick = { },
onElementClick = {
handleClickingItem(it)
},
)
screenState.update { state ->
state.copy(cardDetails = cardDetails)
}
}
@ -107,20 +126,49 @@ internal class CardSettingsViewModel @Inject constructor(
when (item) {
is CardInfo.ChangeAccessCode -> {
Analytics.send(Settings.CardSettings.ButtonChangeUserCode(AnalyticsParam.UserCode.AccessCode))
store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
changeAccessCode()
}
is CardInfo.ResetToFactorySettings -> {
Analytics.send(Settings.CardSettings.ButtonFactoryReset())
store.dispatch(DetailsAction.ResetToFactory.Start)
store.dispatchNavigationAction {
push(route = AppRoute.ResetToFactory(userWalletId))
}
}
is CardInfo.SecurityMode -> {
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
store.dispatchNavigationAction {
push(route = AppRoute.DetailsSecurity(userWalletId))
}
}
is CardInfo.AccessCodeRecovery -> {
store.dispatch(DetailsAction.AccessCodeRecovery.Open)
store.dispatchNavigationAction {
push(route = AppRoute.AccessCodeRecovery(userWalletId))
}
}
else -> {}
}
}
private fun changeAccessCode() = viewModelScope.launch {
val card = getUserWallet().scanResponse.card
when (val result = tangemSdkManager.setAccessCode(card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {
Timber.e("Failed to change access code: ${result.error}")
}
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}
}

View file

@ -1,54 +1,33 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.tangem.core.navigation.NavigationAction
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
class AccessCodeRecoveryFragment : ComposeFragment() {
private val viewModel = AccessCodeRecoveryViewModel(store)
private var screenState: MutableState<AccessCodeRecoveryScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
private val viewModel: AccessCodeRecoveryViewModel by viewModels()
@Inject
override lateinit var uiDependencies: UiDependencies
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
AccessCodeRecoveryScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value =
viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery)
}
}

View file

@ -1,10 +1,6 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
@ -57,7 +53,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = { state.onSaveChangesClick(state.enabledSelection) },
onClick = { state.onSaveChangesClick() },
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
)
}

View file

@ -11,6 +11,6 @@ data class AccessCodeRecoveryScreenState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
val isSaveChangesEnabled: Boolean,
val onSaveChangesClick: (Boolean) -> Unit,
val onSaveChangesClick: () -> Unit,
val onOptionClick: (Boolean) -> Unit,
)

View file

@ -1,30 +1,91 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
class AccessCodeRecoveryViewModel(val store: Store<AppState>) {
@HiltViewModel
internal class AccessCodeRecoveryViewModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tangemSdkManager: TangemSdkManager,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
fun updateState(state: AccessCodeRecoveryState?): AccessCodeRecoveryScreenState {
// We shouldn't get to this screen here when this state is null
return if (state == null) {
AccessCodeRecoveryScreenState(
enabledOnCard = false,
enabledSelection = false,
isSaveChangesEnabled = false,
onSaveChangesClick = {},
onOptionClick = {},
)
} else {
AccessCodeRecoveryScreenState(
enabledOnCard = state.enabledOnCard,
enabledSelection = state.enabledSelection,
isSaveChangesEnabled = state.enabledOnCard != state.enabledSelection,
onSaveChangesClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SaveChanges(it)) },
onOptionClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SelectOption(it)) },
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.AccessCodeRecovery.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("UserWalletId is required for AccessCodeRecoveryViewModel")
val screenState = MutableStateFlow(
value = getInitialState(),
)
private fun getInitialState(): AccessCodeRecoveryScreenState {
val userWallet = getUserWallet()
val isEnabled = isAccessCodeRecoveryEnabled(
typeResolver = userWallet.scanResponse.cardTypesResolver,
card = userWallet.scanResponse.card,
)
return AccessCodeRecoveryScreenState(
enabledOnCard = isEnabled,
enabledSelection = isEnabled,
isSaveChangesEnabled = false,
onSaveChangesClick = ::saveChanges,
onOptionClick = ::selectOption,
)
}
private fun saveChanges() = viewModelScope.launch {
val userWallet = getUserWallet()
val isEnabled = screenState.value.enabledSelection
tangemSdkManager
.setAccessCodeRecoveryEnabled(userWallet.cardId, isEnabled)
.doOnSuccess {
Analytics.send(
Settings.CardSettings.AccessCodeRecoveryChanged(
AnalyticsParam.AccessCodeRecoveryStatus.from(isEnabled),
),
)
store.dispatchNavigationAction(AppRouter::pop)
}
}
private fun selectOption(isEnabled: Boolean) {
screenState.update {
it.copy(
isSaveChangesEnabled = isEnabled != it.enabledOnCard,
)
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Unable to get user wallet $userWalletId: $it")
}
}
}

View file

@ -13,8 +13,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.wallet.R
@Composable
@ -24,31 +24,42 @@ internal fun SettingsScreensScaffold(
modifier: Modifier = Modifier,
@StringRes titleRes: Int? = null,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
addBottomInsets: Boolean = true,
fab: @Composable () -> Unit = {},
) {
val state = rememberScaffoldState(snackbarHostState = snackbarHostState)
val backgroundColor = TangemTheme.colors.background.secondary
BackHandler(onBack = onBackClick)
SystemBarsEffect {
setSystemBarsColor(backgroundColor)
}
Scaffold(
scaffoldState = state,
topBar = {
EmptyTopBarWithNavigation(
modifier = Modifier.statusBarsPadding(),
onBackClick = onBackClick,
backgroundColor = backgroundColor,
)
},
modifier = modifier.systemBarsPadding(),
modifier = modifier,
contentWindowInsets = WindowInsetsZero,
backgroundColor = backgroundColor,
floatingActionButton = fab,
content = { paddings ->
floatingActionButton = {
Box(modifier = Modifier.navigationBarsPadding()) {
fab()
}
},
content = { paddingValues ->
Column(
modifier = Modifier
.padding(paddings)
.run {
if (addBottomInsets) {
navigationBarsPadding()
} else {
this
}
}
.padding(paddingValues)
.fillMaxSize(),
) {
if (titleRes != null) {
@ -81,9 +92,11 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
@Composable
internal fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
backgroundColor: Color = TangemTheme.colors.background.primary,
) {
TopAppBar(
modifier = modifier,
title = { },
navigationIcon =
{

View file

@ -0,0 +1,13 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
internal fun isAccessCodeRecoveryAllowed(typeResolver: CardTypesResolver): Boolean = typeResolver.isWallet2()
internal fun isAccessCodeRecoveryEnabled(typeResolver: CardTypesResolver, card: CardDTO): Boolean =
if (typeResolver.isWallet2()) {
card.userSettings?.isUserCodeRecoveryAllowed ?: false
} else {
false
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal fun getResetToFactoryDescription(card: CardDTO, typesResolver: CardTypesResolver): TextReference {
return if (card.backupStatus?.isActive != true || typesResolver.isTangemTwins()) {
TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
} else {
TextReference.Res(R.string.reset_card_with_backup_to_factory_message)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.details.redux.SecurityOption
import java.util.EnumSet
internal fun getCurrentSecurityOption(card: CardDTO): SecurityOption = when {
card.isAccessCodeSet -> SecurityOption.AccessCode
card.isPasscodeSet == true -> SecurityOption.PassCode
else -> SecurityOption.LongTap
}
internal fun getAllowedSecurityOptions(
card: CardDTO,
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption,
): EnumSet<SecurityOption> = when {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> EnumSet.of(SecurityOption.LongTap)
card.settings.isBackupAllowed -> EnumSet.of(currentSecurityOption)
else -> prepareAllowedSecurityOptions(
cardTypesResolver = cardTypesResolver,
currentSecurityOption = currentSecurityOption,
)
}
private fun prepareAllowedSecurityOptions(
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
if (cardTypesResolver.isTangemTwins()) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (currentSecurityOption == SecurityOption.PassCode) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
return allowedSecurityOptions
}

View file

@ -3,13 +3,14 @@ package com.tangem.tap.features.details.ui.details
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
@ -45,7 +46,7 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState
DetailsScreen(
modifier = modifier,
state = detailsViewModel.detailsScreenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}

View file

@ -67,7 +67,7 @@ internal sealed class SettingsItem(
data class CardSettings(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_card_settings,
iconResId = R.drawable.ic_card_settings_24,
title = resourceReference(R.string.card_settings_title),
)
@ -95,7 +95,7 @@ internal sealed class SettingsItem(
data class ReferralProgram(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_add_friends,
iconResId = R.drawable.ic_add_friends_24,
title = resourceReference(R.string.details_referral_title),
)

View file

@ -3,11 +3,11 @@ package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
@ -16,15 +16,14 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.scope
@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
import timber.log.Timber
// TODO: change to Android ViewModel [REDACTED_JIRA]
internal class DetailsViewModel(
private val store: Store<AppState>,
private val walletsRepository: WalletsRepository,
@ -91,10 +89,6 @@ internal class DetailsViewModel(
SettingsItem.AppSettings(::navigateToAppSettings)
.let(::add)
// removed chat in task [REDACTED_TASK_KEY]
// SettingsItem.Chat(::navigateToChat)
// .let(::add)
SettingsItem.SendFeedback(::sendFeedback)
.let(::add)
@ -128,30 +122,43 @@ internal class DetailsViewModel(
}
private fun navigateToTesterMenu() {
store.state.daggerGraphState.testerRouter?.startTesterScreen()
store.dispatchNavigationAction {
push(AppRoute.TesterMenu)
}
}
private fun navigateToToS() {
store.dispatchOnMain(DisclaimerAction.Show(AppScreen.Details))
store.dispatchOnMain(DisclaimerAction.Show(DisclaimerSource.Details))
}
private fun navigateToReferralProgram() {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
val userWallet = userWalletsListManager.selectedUserWalletSync
?: error("Selected wallet must be not null")
store.dispatchNavigationAction { push(AppRoute.ReferralProgram(userWallet.walletId)) }
}
private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
store.dispatchOnMain(
GlobalAction.SendEmail(
feedbackData = FeedbackEmail(),
scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
private fun navigateToAppSettings() {
Analytics.send(Settings.ButtonAppSettings())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppSettings))
store.dispatchNavigationAction { push(AppRoute.AppSettings) }
}
private fun navigateToCardSettings() {
val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId
?: error("UserWalletId must be not null")
Analytics.send(Settings.ButtonCardSettings())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.CardSettings))
store.dispatchNavigationAction { push(AppRoute.CardSettings(userWalletId)) }
}
private fun linkMoreCards() {
@ -164,7 +171,7 @@ internal class DetailsViewModel(
val scanResponse = selectedUserWallet.scanResponse
Analytics.addContext(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) }
}
private fun scanAndSaveUserWallet() {
@ -174,12 +181,12 @@ internal class DetailsViewModel(
private fun navigateToWalletConnect() {
Analytics.send(Settings.ButtonWalletConnect())
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
store.dispatchNavigationAction { push(AppRoute.WalletConnectSessions) }
}
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
Analytics.send(Settings.ButtonSocialNetwork(link.network))
store.dispatchOnMain(NavigationAction.OpenUrl(link.url))
store.dispatchOpenUrl(link.url)
}
private fun getSocialLinks(): ImmutableList<SocialNetworkLink> {

View file

@ -1,56 +1,34 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.MutableStateFlow
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
internal class ResetCardFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: ResetCardViewModel by viewModels()
private var screenState = MutableStateFlow<ResetCardScreenState>(ResetCardScreenState.InitialState)
@Composable
override fun ScreenContent(modifier: Modifier) {
val state = screenState.collectAsStateWithLifecycle().value
val state by viewModel.screenState.collectAsStateWithLifecycle()
ResetCardScreen(
state = state,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
modifier = modifier,
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState)
}
}

View file

@ -20,24 +20,19 @@ import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.ResetCardScreenContent.Dialog as ResetCardDialog
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog
@Composable
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
modifier = modifier,
content = {
when (state) {
is ResetCardScreenState.ResetCardScreenContent -> ResetCardView(state = state)
ResetCardScreenState.InitialState -> {
// do nothing for now, just white screen
}
}
ResetCardView(state = state)
},
onBackClick = onBackClick,
)
when (val dialog = (state as? ResetCardScreenState.ResetCardScreenContent)?.dialog) {
when (val dialog = state.dialog) {
is ResetCardDialog.StartReset,
is ResetCardDialog.ContinueReset,
is ResetCardDialog.InterruptedReset,
@ -48,7 +43,7 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
}
@Composable
private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) {
private fun ResetCardView(state: ResetCardScreenState) {
val scrollState = rememberScrollState()
Column(
@ -110,7 +105,7 @@ private fun Description(text: TextReference) {
}
@Composable
private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) {
private fun Conditions(state: ResetCardScreenState) {
state.warningsToShow.forEach {
when (it) {
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
@ -120,7 +115,6 @@ private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) {
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
}
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
@ -192,7 +186,7 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
}
@Composable
private fun CommonResetDialog(dialog: ResetCardScreenState.ResetCardScreenContent.Dialog) {
private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
BasicDialog(
title = stringResource(dialog.titleResId),
message = stringResource(dialog.messageResId),
@ -231,10 +225,13 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
.background(TangemTheme.colors.background.secondary),
) {
ResetCardScreen(
state = ResetCardScreenState.ResetCardScreenContent(
accepted = true,
state = ResetCardScreenState(
resetButtonEnabled = true,
showResetPasswordButton = false,
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
acceptCondition1Checked = false,
acceptCondition2Checked = false,
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},
onResetButtonClick = {},

View file

@ -4,63 +4,57 @@ import androidx.annotation.StringRes
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal sealed class ResetCardScreenState {
internal data class ResetCardScreenState(
val resetButtonEnabled: Boolean,
val descriptionText: TextReference,
val warningsToShow: List<WarningsToReset>,
val showResetPasswordButton: Boolean,
val acceptCondition1Checked: Boolean,
val acceptCondition2Checked: Boolean,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
val dialog: Dialog?,
) {
object InitialState : ResetCardScreenState()
sealed class Dialog(
@StringRes val titleResId: Int,
@StringRes val messageResId: Int,
) {
data class ResetCardScreenContent(
val accepted: Boolean = false,
val descriptionText: TextReference,
val warningsToShow: List<WarningsToReset>,
val acceptCondition1Checked: Boolean = false,
val acceptCondition2Checked: Boolean = false,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
val dialog: Dialog? = null,
) : ResetCardScreenState() {
val resetButtonEnabled: Boolean
get() = accepted
abstract val onConfirmClick: () -> Unit
abstract val onDismiss: () -> Unit
sealed class Dialog(
@StringRes val titleResId: Int,
@StringRes val messageResId: Int,
data class StartReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.common_attention,
messageResId = R.string.card_settings_action_sheet_title,
)
data class ContinueReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_continue_reset_alert_title,
messageResId = R.string.card_settings_continue_reset_alert_message,
)
data class InterruptedReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_interrupted_reset_alert_title,
messageResId = R.string.card_settings_interrupted_reset_alert_message,
)
data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog(
titleResId = R.string.card_settings_completed_reset_alert_title,
messageResId = R.string.card_settings_completed_reset_alert_message,
) {
abstract val onConfirmClick: () -> Unit
abstract val onDismiss: () -> Unit
data class StartReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.common_attention,
messageResId = R.string.card_settings_action_sheet_title,
)
data class ContinueReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_continue_reset_alert_title,
messageResId = R.string.card_settings_continue_reset_alert_message,
)
data class InterruptedReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_interrupted_reset_alert_title,
messageResId = R.string.card_settings_interrupted_reset_alert_message,
)
data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog(
titleResId = R.string.card_settings_completed_reset_alert_title,
messageResId = R.string.card_settings_completed_reset_alert_message,
) {
override val onDismiss: () -> Unit = {}
}
override val onDismiss: () -> Unit = {}
}
}

View file

@ -1,104 +1,145 @@
package com.tangem.tap.features.details.ui.resetcard
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import com.tangem.tap.features.details.redux.ResetCardDialog
import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog
@Suppress("LongParameterList")
@HiltViewModel
internal class ResetCardViewModel @Inject constructor(
private val resetCardFeatureToggles: ResetCardFeatureToggles,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val resetCardUseCase: ResetCardUseCase,
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val userWalletsListManager: UserWalletsListManager,
private val analyticsEventHandler: AnalyticsEventHandler,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse
?: error("ScanResponse can't be null")
private val currentUserWalletId = createUserWalletId(firstCardScanResponse)
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.ResetToFactory.USER_WALLET_ID)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID must be provided for ResetCardViewModel")
// TODO: move logic to separate domain entity
private var resetBackupCardCount = 0
fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
val descriptionText = state?.cardInfo
?.toResetCardDescriptionText()
?: TextReference.Str(value = "")
val screenState: MutableStateFlow<ResetCardScreenState> = MutableStateFlow(
value = getInitialState(),
)
private fun getInitialState(): ResetCardScreenState {
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
val card = userWallet.scanResponse.card
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
val descriptionText = getResetToFactoryDescription(card, cardTypesResolver)
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val showResetPasswordButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
val warningsToShow = buildList {
add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS)
if (state?.isShowPasswordResetRadioButton == true) {
if (showResetPasswordButton) {
add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE)
}
}
return ResetCardScreenState.ResetCardScreenContent(
accepted = state?.resetButtonEnabled ?: false,
return ResetCardScreenState(
resetButtonEnabled = false,
descriptionText = descriptionText,
warningsToShow = warningsToShow,
acceptCondition1Checked = state?.condition1Checked ?: false,
acceptCondition2Checked = state?.condition2Checked ?: false,
onAcceptCondition1ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition1(it)) },
onAcceptCondition2ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition2(it)) },
onResetButtonClick = { showDialog(CardSettingsDialog.StartResetDialog) },
dialog = state?.dialog?.let(::createDialog),
showResetPasswordButton = showResetPasswordButton,
acceptCondition1Checked = false,
acceptCondition2Checked = false,
onAcceptCondition1ToggleClick = ::toggleFirstCondition,
onAcceptCondition2ToggleClick = ::toggleSecondCondition,
onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) },
dialog = null,
)
}
private fun createDialog(dialog: CardSettingsDialog): ResetCardScreenState.ResetCardScreenContent.Dialog {
private fun toggleFirstCondition(isAccepted: Boolean) {
screenState.update { prevState ->
val resetButtonEnabled = if (prevState.showResetPasswordButton) {
isAccepted && prevState.acceptCondition2Checked
} else {
isAccepted
}
prevState.copy(
acceptCondition1Checked = isAccepted,
resetButtonEnabled = resetButtonEnabled,
)
}
}
private fun toggleSecondCondition(isAccepted: Boolean) {
screenState.update { prevState ->
val resetButtonEnabled = prevState.acceptCondition1Checked && isAccepted
prevState.copy(
acceptCondition2Checked = isAccepted,
resetButtonEnabled = resetButtonEnabled,
)
}
}
private fun createDialog(dialog: ResetCardDialog): ResetCardScreenState.Dialog {
return when (dialog) {
CardSettingsDialog.StartResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.StartReset(
ResetCardDialog.StartResetDialog -> {
ResetCardScreenState.Dialog.StartReset(
onConfirmClick = ::onStartResetClick,
onDismiss = ::dismissDialog,
)
}
CardSettingsDialog.ContinueResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.ContinueReset(
ResetCardDialog.ContinueResetDialog -> {
ResetCardScreenState.Dialog.ContinueReset(
onConfirmClick = ::onContinueResetClick,
onDismiss = ::onContinueResetDialogDismiss,
)
}
CardSettingsDialog.InterruptedResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset(
ResetCardDialog.InterruptedResetDialog -> {
ResetCardScreenState.Dialog.InterruptedReset(
onConfirmClick = ::onContinueResetClick,
onDismiss = ::onInterruptedResetDialogDismiss,
)
}
CardSettingsDialog.CompletedResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.CompletedReset(
ResetCardDialog.CompletedResetDialog -> {
ResetCardScreenState.Dialog.CompletedReset(
onConfirmClick = ::dismissAndFinishFullReset,
)
}
@ -108,21 +149,26 @@ internal class ResetCardViewModel @Inject constructor(
private fun onStartResetClick() {
dismissDialog()
if (resetCardFeatureToggles.isFullResetEnabled) {
makeFullReset()
} else {
store.dispatch(ResetToFactory.Proceed)
}
makeFullReset()
}
private fun makeFullReset() {
viewModelScope.launch {
resetCardUseCase(card = firstCardScanResponse.card).onRight {
deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId)
deleteWalletUseCase(currentUserWalletId)
val userWallet = getUserWallet()
val scanResponse = userWallet.scanResponse
resetCardUseCase(card = scanResponse.card).onRight {
deleteSavedAccessCodesUseCase(scanResponse.card.cardId)
val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse {
Timber.e("Unable to delete user wallet: $it")
return@launch
}
if (hasUserWallets) {
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrElse {
error("Failed to get selected wallet: $it")
}
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
if (newSelectedWallet != null) {
store.onUserWalletSelected(newSelectedWallet)
}
@ -137,10 +183,11 @@ internal class ResetCardViewModel @Inject constructor(
dismissDialog()
viewModelScope.launch {
val userWallet = getUserWallet()
resetCardUseCase(
cardNumber = resetBackupCardCount + 1,
card = firstCardScanResponse.card,
userWalletId = currentUserWalletId,
card = userWallet.scanResponse.card,
userWalletId = userWalletId,
)
.onRight {
resetBackupCardCount++
@ -149,14 +196,14 @@ internal class ResetCardViewModel @Inject constructor(
checkRemainingBackupCards()
}
.onLeft { showDialog(CardSettingsDialog.InterruptedResetDialog) }
.onLeft { showDialog(ResetCardDialog.InterruptedResetDialog) }
}
}
private fun onContinueResetDialogDismiss() {
dismissDialog()
showDialog(CardSettingsDialog.InterruptedResetDialog)
showDialog(ResetCardDialog.InterruptedResetDialog)
}
private fun onInterruptedResetDialogDismiss() {
@ -166,20 +213,26 @@ internal class ResetCardViewModel @Inject constructor(
}
private fun checkRemainingBackupCards() {
val backupCardsCount = firstCardScanResponse.getBackupCardsCount()
val backupCardsCount = getUserWallet().scanResponse.getBackupCardsCount()
when {
backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog)
backupCardsCount > resetBackupCardCount -> showDialog(ResetCardDialog.ContinueResetDialog)
backupCardsCount == resetBackupCardCount -> {
analyticsEventHandler.send(
event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1),
)
showDialog(CardSettingsDialog.CompletedResetDialog)
showDialog(ResetCardDialog.CompletedResetDialog)
}
else -> finishFullReset()
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
}
private fun dismissAndFinishFullReset() {
dismissDialog()
@ -190,23 +243,23 @@ internal class ResetCardViewModel @Inject constructor(
val newSelectedWallet = userWalletsListManager.selectedUserWalletSync
if (newSelectedWallet != null) {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
} else {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Welcome))
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
}
}
}
private fun showDialog(dialog: CardSettingsDialog) {
store.dispatch(ResetToFactory.ShowDialog(dialog))
private fun showDialog(dialog: ResetCardDialog) {
screenState.update { it.copy(dialog = createDialog(dialog)) }
}
private fun dismissDialog() {
store.dispatch(ResetToFactory.DismissDialog)
screenState.update { it.copy(dialog = null) }
}
private fun ScanResponse.getBackupCardsCount(): Int {
@ -220,9 +273,4 @@ internal class ResetCardViewModel @Inject constructor(
-> 0
}
}
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId {
return UserWalletIdBuilder.scanResponse(scanResponse).build()
?: error("UserWalletId can't be null")
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.details.ui.resetcard.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.details.ui.resetcard.featuretoggles.DefaultResetCardFeatureToggles
import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ResetCardModule {
@Provides
@Singleton
fun provideResetCardFeatureToggles(featureTogglesManager: FeatureTogglesManager): ResetCardFeatureToggles {
return DefaultResetCardFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.features.details.ui.resetcard.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultResetCardFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : ResetCardFeatureToggles {
override val isFullResetEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "FULL_RESET_ENABLED")
}

View file

@ -1,6 +0,0 @@
package com.tangem.tap.features.details.ui.resetcard.featuretoggles
interface ResetCardFeatureToggles {
val isFullResetEnabled: Boolean
}

View file

@ -1,54 +1,34 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.tangem.core.navigation.NavigationAction
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
internal class SecurityModeFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel = SecurityModeViewModel(store)
private var screenState: MutableState<SecurityModeScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
private val viewModel: SecurityModeViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
SecurityModeScreen(
modifier = modifier,
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState?.manageSecurityState)
}
}

View file

@ -1,29 +1,109 @@
package com.tangem.tap.features.details.ui.securitymode
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.ManageSecurityState
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Store
import com.tangem.tap.features.details.ui.common.utils.getAllowedSecurityOptions
import com.tangem.tap.features.details.ui.common.utils.getCurrentSecurityOption
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
internal class SecurityModeViewModel(val store: Store<AppState>) {
@HiltViewModel
internal class SecurityModeViewModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tangemSdkManager: TangemSdkManager,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.DetailsSecurity.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("UserWalletId is required for SecurityModeViewModel")
val screenState = MutableStateFlow(
value = getInitialState(),
)
private fun getInitialState(): SecurityModeScreenState {
val userWallet = getUserWallet()
val scanResponse = userWallet.scanResponse
val card = scanResponse.card
val cardTypesResolver = scanResponse.cardTypesResolver
val currentSecurityOption = getCurrentSecurityOption(card)
val allowedSecurityOptions = getAllowedSecurityOptions(card, cardTypesResolver, currentSecurityOption)
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
if (state == null) {
return SecurityModeScreenState(
availableOptions = emptyList(),
selectedSecurityMode = SecurityOption.LongTap,
isSaveChangesEnabled = false,
onNewModeSelected = {},
onSaveChangesClicked = {},
)
}
return SecurityModeScreenState(
availableOptions = state.allowedOptions.toList(),
selectedSecurityMode = state.selectedOption,
isSaveChangesEnabled = state.selectedOption != state.currentOption,
onNewModeSelected = { store.dispatch(DetailsAction.ManageSecurity.SelectOption(it)) },
onSaveChangesClicked = { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) },
availableOptions = allowedSecurityOptions.toList(),
selectedSecurityMode = currentSecurityOption,
isSaveChangesEnabled = false,
onNewModeSelected = ::selectOption,
onSaveChangesClicked = ::saveChanges,
)
}
private fun selectOption(securityOption: SecurityOption) {
screenState.update { state ->
state.copy(
selectedSecurityMode = securityOption,
isSaveChangesEnabled = securityOption != getCurrentSecurityOption(getUserWallet().scanResponse.card),
)
}
}
private fun saveChanges() {
val userWallet = getUserWallet()
val cardId = userWallet.cardId
val selectedOption = screenState.value.selectedSecurityMode
viewModelScope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
}
val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
when (result) {
is CompletionResult.Success -> {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
store.dispatchNavigationAction(AppRouter::pop)
}
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
}
}
else -> Unit
}
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Unable to get user wallet $userWalletId: $it")
}
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.features.details.ui.utils
import com.tangem.tap.features.details.redux.CardInfo
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal fun CardInfo.toResetCardDescriptionText(): TextReference {
return if (!this.hasBackup || this.isTwin) {
TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
} else {
TextReference.Res(R.string.reset_card_with_backup_to_factory_message)
}
}

View file

@ -6,11 +6,12 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
@ -41,7 +42,7 @@ internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<Wallet
modifier = modifier,
state = state,
onBackClick = {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
)
}

View file

@ -2,12 +2,10 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.common.routing.AppRoute
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
@ -22,14 +20,9 @@ object ClipboardOrScanQrDialog {
store.dispatch(WalletConnectAction.OpenSession(wcUri))
}
setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ ->
store.dispatch(
NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT,
),
),
)
store.dispatchNavigationAction {
push(AppRoute.QrScanning(source = SourceType.WALLET_CONNECT))
}
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.core.navigation.AppScreen
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.disclaimer.Disclaimer
import org.rekotlin.Action
@ -10,7 +9,7 @@ sealed class DisclaimerAction : Action {
data class SetDisclaimer(val disclaimer: Disclaimer) : DisclaimerAction()
data class Show(
val fromScreen: AppScreen,
val from: DisclaimerSource,
val callback: DisclaimerCallback? = null,
) : DisclaimerAction()

View file

@ -1,7 +1,8 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.mainScope
import com.tangem.tap.store
@ -25,17 +26,19 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
when (action) {
is DisclaimerAction.Show -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
store.dispatchNavigationAction {
push(AppRoute.Disclaimer(isTosAccepted = action.from == DisclaimerSource.Details))
}
}
is DisclaimerAction.AcceptDisclaimer -> {
mainScope.launch {
state.disclaimer.accept()
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
state.callback?.onAccept?.invoke()
}
}
is DisclaimerAction.OnBackPressed -> {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
state.callback?.onDismiss?.invoke()
}
}

View file

@ -17,7 +17,7 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
disclaimer = action.disclaimer,
)
is DisclaimerAction.Show -> disclaimerState.copy(
showedFromScreen = action.fromScreen,
showedFrom = action.from,
callback = action.callback,
)
is DisclaimerAction.AcceptDisclaimer, is DisclaimerAction.OnBackPressed -> disclaimerState.copy(

View file

@ -0,0 +1,5 @@
package com.tangem.tap.features.disclaimer.redux
enum class DisclaimerSource {
Home, Details
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.AppScreen
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.DummyDisclaimer
@ -9,7 +9,7 @@ import org.rekotlin.StateType
data class DisclaimerState(
val disclaimer: Disclaimer = DummyDisclaimer(),
val showedFromScreen: AppScreen = AppScreen.Home,
val showedFrom: DisclaimerSource = DisclaimerSource.Home,
val callback: DisclaimerCallback? = null,
val progressState: ProgressState? = null,
) : StateType

View file

@ -7,7 +7,7 @@ import android.webkit.WebView
import androidx.lifecycle.lifecycleScope
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.beginDelayedTransition
@ -18,6 +18,7 @@ import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.store
import com.tangem.wallet.R
@ -76,17 +77,14 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
override fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
when (store.state.disclaimerState.showedFromScreen) {
AppScreen.Home -> {
when (store.state.disclaimerState.showedFrom) {
DisclaimerSource.Home -> {
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
}
AppScreen.Details -> {
DisclaimerSource.Details -> {
super.configureTransitions()
}
else -> {
/* no-op */
}
}
}

View file

@ -6,17 +6,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.tokens.TokensAction
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
@ -43,15 +42,12 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
@Composable
override fun ScreenContent(modifier: Modifier) {
BackHandler(onBack = requireActivity()::finish)
SystemBarsEffect {
setSystemBarsColor(color = Color.Transparent, darkIcons = false)
}
SystemBarsIconsDisposable(darkIcons = false)
ScreenContent()
}
override fun onStart() {
super.onStart()
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
store.subscribe(subscriber = this) { state ->
state
@ -86,7 +82,7 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
},
onSearchTokensClick = {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatch(NavigationAction.NavigateTo(AppScreen.ManageTokens))
store.dispatchNavigationAction { push(AppRoute.ManageTokens) }
store.dispatch(TokensAction.SetArgs.ReadAccess)
},
)

View file

@ -4,11 +4,10 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
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.wallets.builder.UserWalletBuilder
@ -119,7 +118,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
}
.doOnResult {
navigateTo(AppScreen.Wallet)
navigateTo(AppRoute.Wallet)
}
}
@ -143,8 +142,8 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
}
}
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
private suspend fun navigateTo(route: AppRoute) {
store.dispatchNavigationAction { push(route) }
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}

View file

@ -3,11 +3,10 @@ package com.tangem.tap.features.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -16,9 +15,11 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.tap.store
@ -34,7 +35,7 @@ import javax.inject.Inject
internal class MainViewModel @Inject constructor(
private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase,
private val listenToFlipsUseCase: ListenToFlipsUseCase,
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase,
private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
@ -44,6 +45,8 @@ internal class MainViewModel @Inject constructor(
private val sendFeatureToggles: SendFeatureToggles,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggles: StakingFeatureToggles,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
@ -70,6 +73,10 @@ internal class MainViewModel @Inject constructor(
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
if (stakingFeatureToggles.isStakingEnabled) {
fetchStakingTokens()
}
viewModelScope.launch(dispatchers.main) {
deleteDeprecatedLogsUseCase()
}
@ -115,6 +122,14 @@ internal class MainViewModel @Inject constructor(
}
}
private fun fetchStakingTokens() {
viewModelScope.launch(dispatchers.main) {
fetchStakingTokensUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") }
.onRight { Timber.d("Staking token list was fetched successfully") }
}
}
private fun updateSendFeatureToggle() {
viewModelScope.launch(dispatchers.main) {
sendFeatureToggles.fetchNewSendEnabled()
@ -136,7 +151,7 @@ internal class MainViewModel @Inject constructor(
if (state.value.modalNotification?.isShow != true && !it.isUpdateFromToast) {
listenToFlipsUseCase.changeUpdateEnabled(false)
stateHolder.updateWithHiddenBalancesNotification()
reduxNavController.navigate(NavigationAction.NavigateTo(AppScreen.ModalNotification))
router.push(AppRoute.ModalNotification)
}
}
.launchIn(viewModelScope)

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.redux.StateDialog
/**
[REDACTED_AUTHOR]

View file

@ -3,10 +3,10 @@ package com.tangem.tap.features.onboarding
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
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.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
@ -56,18 +56,18 @@ object OnboardingHelper {
}
}
fun whereToNavigate(scanResponse: ScanResponse): AppScreen {
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
return when (val type = scanResponse.productType) {
ProductType.Note -> AppScreen.OnboardingNote
ProductType.Note -> AppRoute.OnboardingNote
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> if (scanResponse.card.settings.isBackupAllowed) {
AppScreen.OnboardingWallet
AppRoute.OnboardingWallet()
} else {
AppScreen.OnboardingOther
AppRoute.OnboardingOther
}
ProductType.Twins -> AppScreen.OnboardingTwins
ProductType.Twins -> AppRoute.OnboardingTwins
ProductType.Start2Coin,
ProductType.Visa,
-> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported")
@ -111,14 +111,14 @@ object OnboardingHelper {
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchNavigationAction { push(AppRoute.Wallet) }
delay(timeMillis = 1_800)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet))
store.dispatchNavigationAction { push(AppRoute.SaveWallet) }
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchNavigationAction { push(AppRoute.Wallet) }
}
}
}

View file

@ -7,15 +7,20 @@ import androidx.core.view.MenuProvider
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class OnboardingMenuProvider : MenuProvider {
class OnboardingMenuProvider(
private val scanResponseProvider: Provider<ScanResponse>,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_onboarding, menu)
}
@ -24,7 +29,12 @@ class OnboardingMenuProvider : MenuProvider {
R.id.menu_item_chat_support -> {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
store.dispatch(
GlobalAction.SendEmail(
feedbackData = SupportInfo(),
scanResponse = scanResponseProvider(),
),
)
true
}
else -> false

View file

@ -9,6 +9,8 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingMainBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
@ -32,7 +34,13 @@ abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_o
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
}
override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider()
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
},
)
protected fun showConfetti(show: Boolean) = with(binding.vConfetti) {
lavConfetti.show(show)

View file

@ -2,8 +2,9 @@ package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
@ -204,7 +205,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
OnboardingDialog.InterruptOnboarding(
onOk = {
OnboardingHelper.onInterrupted()
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
),
)

View file

@ -3,9 +3,10 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
@ -39,6 +40,7 @@ import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import kotlin.reflect.KClass
object TwinCardsMiddleware {
val handler = twinsWalletMiddleware
@ -328,7 +330,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
if (walletsRepository.shouldSaveUserWalletsSync()) {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
}
}
}
@ -352,7 +354,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatchOnMain(NavigationAction.PopBackTo(getPopBackScreen()))
store.dispatchNavigationAction { popTo(routeClass = getPopBackScreen()) }
}
}
store.dispatchDialogShow(OnboardingDialog.InterruptOnboarding(onOkCallback))
@ -362,18 +364,19 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
private fun getPopBackScreen(): AppScreen {
private fun getPopBackScreen(): KClass<out AppRoute> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
return if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
if (isLocked) {
AppScreen.Welcome
AppRoute.Welcome::class
} else {
AppScreen.Wallet
AppRoute.Wallet::class
}
} else {
AppScreen.Home
AppRoute.Home::class
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
@ -35,7 +35,10 @@ private fun internalReduce(action: Action, state: AppState): TwinCardsState {
)
}
is TwinCardsAction.SetStepOfScreen -> {
state = state.copy(currentStep = action.step)
state = state.copy(
currentStep = action.step,
welcomeOnlyScanResponse = (action.step as? TwinCardsStep.WelcomeOnly)?.scanResponse,
)
}
is TwinCardsAction.SetUserUnderstand -> {
state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand)

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
@ -29,6 +29,7 @@ data class TwinCardsState(
val balanceNonCriticalError: TapError? = null,
val balanceCriticalError: TapError? = null,
val showConfetti: Boolean = false,
val welcomeOnlyScanResponse: ScanResponse? = null,
) : StateType {
val steps: List<TwinCardsStep>

View file

@ -6,6 +6,7 @@ import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
@ -25,12 +26,14 @@ import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint
@ -61,6 +64,15 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment<TwinCardsState>(
}
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.twinCardsState.welcomeOnlyScanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
},
)
@Suppress("MagicNumber")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -7,16 +7,16 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.extensions.toHexString
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.Artwork
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.models.Artwork
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
@ -25,10 +25,7 @@ import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
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.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@ -164,7 +161,7 @@ private fun handleWalletAction(action: Action) {
store.dispatch(GlobalAction.Onboarding.Stop)
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
store.dispatch(HomeAction.ReadCard(scope = action.scope))
} else {
val backupState = store.state.onboardingWalletState.backupState
@ -529,7 +526,8 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
addedBackupCardsCount = backupService.addedBackupCardsCount,
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) }
}
is BackupAction.SkipBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
@ -614,7 +612,7 @@ private fun handleOnBackPressed(state: OnboardingWalletState) {
}
BackupStep.Finished -> {
OnboardingHelper.onInterrupted()
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
}
}
}
@ -625,7 +623,7 @@ private fun showInterruptOnboardingDialog() {
onOk = {
OnboardingHelper.onInterrupted()
store.dispatch(BackupAction.DiscardBackup)
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
},
),
)

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import android.net.Uri
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.redux.StateDialog
import org.rekotlin.StateType
/**

View file

@ -1,6 +1,10 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.collectAsState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen
@ -16,6 +20,7 @@ import com.tangem.wallet.R
*/
internal class OnboardingSeedPhraseStateHandler(
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(),
private val activity: Activity,
) {
fun newState(
@ -49,11 +54,16 @@ internal class OnboardingSeedPhraseStateHandler(
val subScreen = viewModel.currentScreen.collectAsState().value
setMainScreenToolbarTitle(walletFragment, subScreen)
onboardingSeedPhraseApi.ScreenContent(
uiState = viewModel.uiState,
subScreen = subScreen,
progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress,
)
TangemTheme(
isDark = isSystemInDarkTheme(),
windowSize = rememberWindowSize(activity = activity),
) {
onboardingSeedPhraseApi.ScreenContent(
uiState = viewModel.uiState,
subScreen = subScreen,
progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress,
)
}
}
}

View file

@ -21,13 +21,13 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.navigation.OnboardingRouter
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter
@ -46,6 +46,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding
@ -66,9 +67,10 @@ class OnboardingWalletFragment :
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
private val canSkipBackup by lazy { arguments?.getBoolean(OnboardingRouter.CAN_SKIP_BACKUP) ?: true }
private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true }
private lateinit var seedPhraseStateHandler: OnboardingSeedPhraseStateHandler
private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler()
private val seedPhraseViewModel by viewModels<SeedPhraseViewModel>()
private lateinit var cardsWidget: WalletCardsWidget
@ -79,6 +81,7 @@ class OnboardingWalletFragment :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
seedPhraseStateHandler = OnboardingSeedPhraseStateHandler(activity = requireActivity())
val newSeedPhraseRouter = makeSeedPhraseRouter()
seedPhraseRouter = newSeedPhraseRouter
@ -113,7 +116,12 @@ class OnboardingWalletFragment :
)
}
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider()
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null")
},
)
private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) {
val viewBackupCount = flCardsContainer.childCount - 1
@ -505,7 +513,13 @@ class OnboardingWalletFragment :
onOpenChat = {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
// changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
store.dispatch(
GlobalAction.SendEmail(
feedbackData = SupportInfo(),
scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null"),
),
)
},
onOpenUriClick = { uri ->
store.dispatchOpenUrl(uri.toString())

View file

@ -23,7 +23,13 @@ object WalletActivationErrorDialog {
setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY]
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
store.dispatch(GlobalAction.SendEmail(SupportInfo()))
store.dispatch(
GlobalAction.SendEmail(
feedbackData = SupportInfo(),
scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)

View file

@ -3,29 +3,22 @@ package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.wallets.builder.UserWalletBuilder
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
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.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
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 {
@ -83,12 +76,12 @@ internal class SaveWalletMiddleware {
Timber.e(error, "Unable to save user wallet")
}
.doOnSuccess { mainScope.launch { store.onUserWalletSelected(userWallet) } }
.doOnResult { store.navigateToWallet() }
.doOnResult { navigateToWallet() }
}
}
private fun enrollBiometrics() {
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
activityResultCaller.openSystemBiometrySettings()
}
private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch {
@ -131,7 +124,7 @@ internal class SaveWalletMiddleware {
)
store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
private fun dismiss(state: SaveWalletState) {
@ -160,13 +153,9 @@ internal class SaveWalletMiddleware {
}
}
private suspend fun Store<AppState>.navigateToWallet() {
dispatchWithMain(
if (store.state.navigationState.backStack.contains(AppScreen.Wallet)) {
NavigationAction.PopBackTo(AppScreen.Wallet)
} else {
NavigationAction.NavigateTo(AppScreen.Wallet)
},
)
private fun navigateToWallet() {
store.dispatchNavigationAction {
replaceAll(AppRoute.Wallet)
}
}
}

View file

@ -7,7 +7,8 @@ import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.ToastNotificationAction
@ -185,12 +186,16 @@ sealed class SendAction : SendScreenAction {
) : Dialog()
sealed class SendTransactionFails : Dialog() {
data class CardSdkError(val error: TangemSdkError) : Dialog()
data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog()
data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog()
data class BlockchainSdkError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
) : Dialog()
}
data class RequestFeeError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
val onRetry: () -> Unit,
) : Dialog()

View file

@ -1,6 +1,9 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
@ -92,6 +95,7 @@ class RequestFeeMiddleware {
dispatch(
SendAction.Dialog.RequestFeeError(
error = blockchainSdkError,
scanResponse = scanResponse,
onRetry = { dispatch(FeeAction.RequestFee) },
),
)

View file

@ -14,10 +14,10 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.demo.DemoTransactionSender
@ -270,7 +270,7 @@ private fun sendTransaction(
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
}
}
is Result.Failure -> {
@ -288,11 +288,25 @@ private fun sendTransaction(
val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext
if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext
dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError))
dispatch(
SendAction.Dialog.SendTransactionFails.CardSdkError(
error = tangemSdkError,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
is BlockchainSdkError.CreateAccountUnderfunded -> {
// from XLM, XRP, Polkadot
dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error))
dispatch(
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
error = error,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
is BlockchainSdkError.Kaspa.UtxoAmountError -> {
dispatch(
@ -326,12 +340,19 @@ private fun sendTransaction(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_done,
messageId = R.string.alert_demo_feature_disabled,
onOk = { dispatch(NavigationAction.PopBackTo()) },
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
),
)
}
else -> {
dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error))
dispatch(
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
error = error,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.common.Strings.STARS
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.AmountAction
@ -11,6 +10,7 @@ import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.utils.StringsSigns.STARS
import java.math.BigDecimal
/**

View file

@ -10,6 +10,7 @@ import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.store
import com.tangem.utils.StringsSigns.LOWER_SIGN
import java.math.BigDecimal
/**
@ -401,11 +402,7 @@ class ReceiptReducer : SendInternalReducer {
}
private fun String.addPrecisionSign(): String {
val result = if (feeState.feeIsApproximate) "$CAN_BE_LOWER_SIGN $this" else this
val result = if (feeState.feeIsApproximate) "$LOWER_SIGN $this" else this
return result.trim()
}
private companion object {
const val CAN_BE_LOWER_SIGN = "<"
}
}

View file

@ -4,8 +4,8 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton

View file

@ -8,7 +8,6 @@ import android.text.method.DigitsKeyListener
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.core.os.bundleOf
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.viewModels
@ -20,19 +19,18 @@ import androidx.recyclerview.widget.RecyclerView
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.sdk.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
@ -162,14 +160,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
imvQrCode.setOnClickListener {
Analytics.send(Token.Send.ButtonQRCode())
store.dispatchOnMain(
NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.SEND,
),
),
)
store.dispatchNavigationAction {
push(AppRoute.QrScanning(source = SourceType.SEND))
}
}
}
@ -370,7 +363,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
override fun handleOnBackPressed() {
val externalTransactionData = store.state.sendState.externalTransactionData
if (externalTransactionData == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
} else {
store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.ui
import androidx.lifecycle.*
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
@ -12,7 +13,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.di.DelayedWork
import com.tangem.tap.features.send.redux.AddressActionUi
@ -52,7 +52,7 @@ internal class SendViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY]
private val cryptoCurrency: CryptoCurrency? = savedStateHandle[AppRoute.Send.CRYPTO_CURRENCY_KEY]
override fun onCreate(owner: LifecycleOwner) {
getBalanceHidingSettingsUseCase()

View file

@ -23,7 +23,12 @@ object RequestFeeErrorDialog {
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
store.dispatch(
GlobalAction.SendEmail(
feedbackData = SendTransactionFailedEmail(errorMessage),
scanResponse = dialog.scanResponse,
),
)
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
setNeutralButton(R.string.common_cancel) { _, _ -> }

View file

@ -8,6 +8,7 @@ import com.tangem.common.module.ModuleMessageConverter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.sdk.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
@ -21,21 +22,21 @@ import com.tangem.wallet.R
*/
object SendTransactionFailsDialog {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog {
return create(context, dialog.error.localizedDescription(context))
return create(context, dialog.error.localizedDescription(context), dialog.scanResponse)
}
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog {
val errorConverter = BlockchainSdkErrorConverter(context)
return create(context, errorConverter.convert(dialog.error))
return create(context, errorConverter.convert(dialog.error), dialog.scanResponse)
}
private fun create(context: Context, errorMessage: String): AlertDialog {
private fun create(context: Context, errorMessage: String, scanResponse: ScanResponse): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage), scanResponse))
}
setPositiveButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }

View file

@ -1,13 +1,10 @@
package com.tangem.tap.features.tokens.impl.presentation
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen
import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel
@ -30,12 +27,7 @@ internal class TokensListFragment : ComposeFragment() {
val viewModel = hiltViewModel<TokensListViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(this)
}
val statusBarColor = TangemTheme.colors.background.secondary
SystemBarsEffect {
setSystemBarsColor(color = statusBarColor)
}
TokensListScreen(
modifier = Modifier.systemBarsPadding(),
stateHolder = viewModel.uiState,
)
}

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.tokens.impl.presentation.router
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
@ -18,11 +20,11 @@ import com.tangem.wallet.R
internal class DefaultTokensListRouter : TokensListRouter {
override fun popBackStack() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
}
override fun openAddCustomTokenScreen() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
store.dispatchNavigationAction { push(AppRoute.AddCustomToken) }
}
override fun showAddressCopiedNotification() {
@ -58,7 +60,7 @@ internal class DefaultTokensListRouter : TokensListRouter {
val alert = AppDialog.SimpleOkDialogRes(
headerId = R.string.common_error,
messageId = R.string.common_unknown_error,
onOk = { store.dispatch(NavigationAction.PopBackTo()) },
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
)
store.dispatchDialogShow(alert)
}

View file

@ -15,7 +15,6 @@ import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
@ -34,6 +33,8 @@ import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
@ -61,32 +62,45 @@ internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modi
Scaffold(
modifier = modifier,
topBar = { TokensListToolbar(state = stateHolder.toolbarState) },
topBar = {
TokensListToolbar(state = stateHolder.toolbarState)
},
floatingActionButton = {
if (stateHolder is TokensListStateHolder.ManageContent) {
val density = LocalDensity.current
val verticalPadding = TangemTheme.dimens.spacing32
SaveChangesButton(
modifier = Modifier.onSizeChanged {
with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding }
},
modifier = Modifier
.navigationBarsPadding()
.onSizeChanged {
with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding }
},
showProgress = stateHolder.isSavingInProgress,
onClick = stateHolder.onSaveButtonClick,
)
}
},
floatingActionButtonPosition = FabPosition.Center,
backgroundColor = TangemTheme.colors.background.primary,
) { scaffoldPadding ->
backgroundColor = TangemTheme.colors.background.secondary,
) { _ ->
val tokens = stateHolder.tokens.collectAsLazyPagingItems()
TokensListContent(
isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible,
tokens = tokens,
scaffoldPadding = scaffoldPadding,
bottomMarginDp = floatingButtonHeight,
)
if (stateHolder !is TokensListStateHolder.ManageContent) {
NavigationBar3ButtonsScrim()
}
Box {
TokensListContent(
isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible,
tokens = tokens,
bottomMarginDp = floatingButtonHeight,
)
if (stateHolder is TokensListStateHolder.ManageContent) {
BottomFade(Modifier.align(Alignment.BottomCenter))
}
}
Crossfade(targetState = stateHolder.isLoading, label = "Update progress bar visibility") {
if (it) {
@ -112,23 +126,24 @@ private fun LoadingContent() {
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun TokensListContent(
isDifferentAddressesBlockVisible: Boolean,
tokens: LazyPagingItems<TokenItemState>,
scaffoldPadding: PaddingValues,
bottomMarginDp: Dp,
) {
val state = rememberLazyListState()
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.imePadding()
.fillMaxSize()
.padding(scaffoldPadding),
.fillMaxSize(),
state = state,
contentPadding = PaddingValues(bottom = bottomMarginDp),
contentPadding = PaddingValues(
bottom = bottomMarginDp + bottomBarHeight,
),
) {
item(
key = "DifferentAddressesWarning$isDifferentAddressesBlockVisible",

View file

@ -1,10 +1,7 @@
package com.tangem.tap.features.tokens.impl.presentation.ui
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -16,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -42,8 +40,15 @@ internal fun TokensListToolbar(state: TokensListToolbarState) {
} else {
AppBarDefaults.TopAppBarElevation
}
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
TopAppBar(
backgroundColor = TangemTheme.colors.background.secondary,
contentPadding = PaddingValues(
start = TangemTheme.dimens.spacing4,
end = TangemTheme.dimens.spacing4,
top = statusBarHeight,
),
elevation = toolbarElevation,
) {
IconButton(onClick = state.onBackButtonClick) {

View file

@ -3,8 +3,8 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.util.derivationStyleProvider
@ -16,7 +16,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -126,7 +126,7 @@ internal class TokensListMigration(
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
return
}

View file

@ -11,9 +11,9 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getGreyedOutIconRes
import com.tangem.domain.card.DerivePublicKeysUseCase
@ -26,7 +26,7 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
import com.tangem.tap.common.extensions.getNetworkName
import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType
@ -326,7 +326,7 @@ internal class TokensListViewModel @Inject constructor(
)
uiState = state.copy(isSavingInProgress = false)
store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
}

View file

@ -1,18 +1,17 @@
package com.tangem.tap.features.wallet.redux.middlewares
import androidx.core.os.bundleOf
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.*
@ -58,6 +57,11 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Sell -> proceedSellAction(action)
is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency)
is TradeCryptoAction.Stake -> openStaking(
userWalletId = action.userWalletId,
cryptoCurrencyId = action.cryptoCurrencyId,
yield = action.yield,
)
is TradeCryptoAction.SendToken -> {
if (isSendRedesignedEnabled) {
handleNewSendToken(action = action)
@ -150,7 +154,7 @@ object TradeCryptoMiddleware {
}
private fun openReceiptUrl(transactionId: String) {
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchNavigationAction(AppRouter::pop)
store.state.globalState.exchangeManager.getSellCryptoReceiptUrl(
action = CurrencyExchangeManager.Action.Sell,
transactionId = transactionId,
@ -158,11 +162,19 @@ object TradeCryptoMiddleware {
}
private fun openSwap(currency: CryptoCurrency) {
val bundle = bundleOf(
SwapFragment.CURRENCY_BUNDLE_KEY to currency,
)
store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) }
}
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) {
store.dispatchNavigationAction {
push(
AppRoute.Staking(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
yield = yield,
),
)
}
}
private fun handleSendToken(action: TradeCryptoAction.SendToken) {
@ -214,11 +226,12 @@ object TradeCryptoMiddleware {
)
}
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
val route = AppRoute.Send(
currency = currency,
userWalletId = action.userWallet.walletId,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
store.dispatchNavigationAction { push(route) }
}
}
@ -285,17 +298,17 @@ object TradeCryptoMiddleware {
)
}
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,
val route = AppRoute.Send(
currency = currency,
userWalletId = action.userWallet.walletId,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
store.dispatchNavigationAction { push(route) }
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
userWalletId = action.userWallet.walletId,
txInfo = action.transactionInfo,
currency = action.tokenCurrency,
)
@ -303,25 +316,26 @@ object TradeCryptoMiddleware {
private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) {
handleNewSend(
userWalletId = action.userWallet.walletId.stringValue,
userWalletId = action.userWallet.walletId,
txInfo = action.transactionInfo,
currency = action.coinStatus.currency,
)
}
private fun handleNewSend(
userWalletId: String,
userWalletId: UserWalletId,
txInfo: TradeCryptoAction.TransactionInfo?,
currency: CryptoCurrency,
) {
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to userWalletId,
SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId,
SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress,
SendRouter.AMOUNT_KEY to txInfo?.amount,
SendRouter.TAG_KEY to txInfo?.tag,
val route = AppRoute.Send(
currency = currency,
userWalletId = userWalletId,
transactionId = txInfo?.transactionId,
destinationAddress = txInfo?.destinationAddress,
amount = txInfo?.amount,
tag = txInfo?.tag,
)
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle))
store.dispatchNavigationAction { push(route) }
}
}

View file

@ -6,11 +6,12 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
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.wallets.builder.UserWalletBuilder
@ -98,7 +99,7 @@ internal class WelcomeMiddleware {
signInType = Basic.SignedIn.SignInType.Biometric,
)
store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchNavigationAction { push(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(userWallet = selectedUserWallet)
@ -130,7 +131,7 @@ internal class WelcomeMiddleware {
.doOnSuccess {
sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card)
store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatchNavigationAction { push(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet = userWallet)
@ -170,7 +171,7 @@ internal class WelcomeMiddleware {
}
.doOnResult {
store.dispatchWithMain(WelcomeAction.CloseError)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
}
}

View file

@ -16,7 +16,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.analytics.events.SignIn
@ -49,19 +48,14 @@ internal class WelcomeFragment : ComposeFragment() {
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val warning by rememberUpdatedState(newValue = state.warning)
val backgroundColor = TangemTheme.colors.background.primary
SystemBarsEffect {
setSystemBarsColor(backgroundColor)
}
BackHandler {
requireActivity().finish()
}
Box(
modifier = modifier
.systemBarsPadding()
.background(backgroundColor),
.background(TangemTheme.colors.background.primary)
.systemBarsPadding(),
) {
WelcomeScreenContent(
showUnlockProgress = state.showUnlockWithBiometricsProgress,
@ -88,8 +82,4 @@ internal class WelcomeFragment : ComposeFragment() {
}
}
}
internal companion object {
const val INITIAL_INTENT_KEY = "intent"
}
}

View file

@ -1,8 +1,11 @@
package com.tangem.tap.features.welcome.ui
import android.content.Intent
import android.os.Bundle
import androidx.lifecycle.*
import com.tangem.common.core.TangemError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.analytics.Analytics
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.tap.common.analytics.events.SignIn
@ -26,7 +29,8 @@ internal class WelcomeViewModel @Inject constructor(
StoreSubscriber<WelcomeState>,
DefaultLifecycleObserver {
private val initialIntent: Intent? = savedStateHandle[WelcomeFragment.INITIAL_INTENT_KEY]
private val initialIntent: SerializableIntent? = savedStateHandle.get<Bundle>(AppRoute.Welcome.INITIAL_INTENT_KEY)
?.unbundle(SerializableIntent.serializer())
private val stateInternal = MutableStateFlow(WelcomeScreenState())
val state: StateFlow<WelcomeScreenState> = stateInternal
@ -38,7 +42,7 @@ internal class WelcomeViewModel @Inject constructor(
initGlobalState()
val welcomeAction = if (initialIntent != null) {
WelcomeAction.ProceedWithIntent(initialIntent)
WelcomeAction.ProceedWithIntent(initialIntent.toIntent())
} else {
WelcomeAction.ProceedWithBiometrics()
}