Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-20 11:41:10 +04:00
commit f58f67b7ff
19 changed files with 1659 additions and 1261 deletions

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.addCustomToken.AddCustomTokenFragment
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
@ -109,6 +110,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.CardSettings -> CardSettingsFragment()
AppScreen.AppSettings -> AppSettingsFragment()
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> {
val featureToggles = store.state.daggerGraphState.get(

View file

@ -18,7 +18,7 @@ enum class AppScreen(
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,
Send,
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory,
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory, AccessCodeRecovery,
AddTokens, AddCustomToken,
WalletConnectSessions,
QrScan,

View file

@ -425,7 +425,7 @@ class DetailsMiddleware {
fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) {
when (action) {
is DetailsAction.AccessCodeRecovery.Open -> {
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery)) Todo: next PR
store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges -> {
scope.launch {

View file

@ -120,6 +120,7 @@ fun CardSettings(state: CardSettingsScreenState) {
is CardInfo.SignedHashes -> 14.dp
is CardInfo.SecurityMode -> 16.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 28.dp
}
val paddingTop = when (it) {
@ -128,6 +129,7 @@ fun CardSettings(state: CardSettingsScreenState) {
is CardInfo.SignedHashes -> 12.dp
is CardInfo.SecurityMode -> 14.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 16.dp
}
Column(

View file

@ -4,6 +4,7 @@ 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
@ -12,6 +13,7 @@ import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null,
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
)
@ -48,6 +50,16 @@ sealed class CardInfo(
clickable = true,
)
class AccessCodeRecovery(val enabled: Boolean) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title),
subtitle = if (enabled) {
TextReference.Res(R.string.common_enabled)
} else {
TextReference.Res(R.string.common_disabled)
},
clickable = true,
)
class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
subtitle = cardInfo.toResetCardDescriptionText(),

View file

@ -16,6 +16,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
return if (state?.manageSecurityState == null) {
CardSettingsScreenState(
cardDetails = null,
accessCodeRecoveryState = null,
onElementClick = {},
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
@ -44,12 +45,15 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
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)
@ -72,6 +76,9 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
}
is CardInfo.AccessCodeRecovery -> {
store.dispatch(DetailsAction.AccessCodeRecovery.Open)
}
else -> {}
}
}

View file

@ -0,0 +1,67 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class AccessCodeRecoveryFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = AccessCodeRecoveryViewModel(store)
private var screenState: MutableState<AccessCodeRecoveryScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
TangemTheme {
AccessCodeRecoveryScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
}
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

@ -0,0 +1,64 @@
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun AccessCodeRecoveryScreen(state: AccessCodeRecoveryScreenState, onBackClick: () -> Unit) {
SettingsScreensScaffold(
content = { AccessCodeRecoveryOptions(state = state) },
onBackClick = onBackClick,
)
}
@Composable
fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(bottom = TangemTheme.dimens.spacing28),
verticalArrangement = Arrangement.SpaceBetween,
) {
ScreenTitle(
titleRes = R.string.card_settings_access_code_recovery_title,
Modifier.padding(bottom = TangemTheme.dimens.spacing36),
)
DetailsRadioButtonElement(
title = stringResource(id = R.string.common_enabled),
subtitle = stringResource(id = R.string.card_settings_access_code_recovery_enabled_description),
selected = state.enabledSelection,
onClick = { state.onOptionClick(true) },
)
DetailsRadioButtonElement(
title = stringResource(id = R.string.common_disabled),
subtitle = stringResource(id = R.string.card_settings_access_code_recovery_disabled_description),
selected = !state.enabledSelection,
onClick = { state.onOptionClick(false) },
)
Spacer(modifier = Modifier.weight(1f))
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = { state.onSaveChangesClick(state.enabledSelection) },
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
/**
* @property enabledOnCard Indicates whether access code recovery is enabled on the card
* @property enabledSelection Represents the currently selected option in the app (not yet saved on the card)
* @property isSaveChangesEnabled Determines if the user is allowed to save their selection to the card
* @property onSaveChangesClick Callback function called when the user wants to apply the selected option
* @property onOptionClick Callback function called when the user selects an option
* */
data class AccessCodeRecoveryScreenState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
val isSaveChangesEnabled: Boolean,
val onSaveChangesClick: (Boolean) -> Unit,
val onOptionClick: (Boolean) -> Unit,
)

View file

@ -0,0 +1,30 @@
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
class AccessCodeRecoveryViewModel(val store: Store<AppState>) {
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)) },
)
}
}
}

View file

@ -3,18 +3,18 @@ package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
@ -25,7 +25,9 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.wallet.R
@Composable
@ -109,26 +111,49 @@ fun EmptyTopBarWithNavigation(
@Composable
fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) {
Button(
PrimaryButtonIconRight(
text = title,
enabled = enabled,
onClick = onClick,
modifier = modifier
.fillMaxWidth(),
icon = painterResource(id = R.drawable.ic_tangem_24),
)
}
@Composable
fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(48.dp),
shape = RoundedCornerShape(12.dp),
enabled = enabled,
colors = ButtonDefaults.buttonColors(
backgroundColor = colorResource(R.color.button_primary),
contentColor = colorResource(R.color.text_primary_2),
disabledBackgroundColor = colorResource(R.color.button_disabled),
disabledContentColor = colorResource(R.color.text_disabled),
),
.selectable(
selected = selected,
onClick = { onClick() },
)
.padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
) {
Text(text = title)
Spacer(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp)
.size(8.dp),
RadioButton(
selected = selected,
onClick = null,
modifier = Modifier.padding(end = 20.dp),
colors = RadioButtonDefaults.colors(
unselectedColor = colorResource(id = R.color.icon_secondary),
selectedColor = colorResource(id = R.color.icon_accent),
),
)
Icon(painter = painterResource(id = R.drawable.ic_tangem_24), contentDescription = "")
Column {
Text(
text = title,
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = Modifier.size(4.dp))
Text(
text = subtitle,
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
}
}

View file

@ -2,27 +2,19 @@ package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -74,39 +66,12 @@ fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
SecurityOption.AccessCode -> R.string.details_manage_security_access_code_description
}
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = selected,
onClick = { state.onNewModeSelected(option) },
)
.padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
) {
RadioButton(
selected = selected,
onClick = null,
modifier = Modifier.padding(end = 20.dp),
colors = RadioButtonDefaults.colors(
unselectedColor = colorResource(id = R.color.icon_secondary),
selectedColor = colorResource(id = R.color.icon_accent),
),
)
Column {
Text(
text = stringResource(id = title),
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = Modifier.size(4.dp))
Text(
text = stringResource(id = subtitle),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
}
DetailsRadioButtonElement(
title = stringResource(id = title),
subtitle = stringResource(id = subtitle),
selected = selected,
onClick = { state.onNewModeSelected(option) },
)
}
@Preview