Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-22 11:36:12 +03:00
parent 3fc34fac64
commit 0dd4d45b62
11 changed files with 141 additions and 67 deletions

View file

@ -61,6 +61,9 @@ class TangemSdkManager(
val biometricManager: BiometricManager
get() = tangemSdk.biometricManager
val userCodeRequestPolicy: UserCodeRequestPolicy
get() = tangemSdk.config.userCodeRequestPolicy
suspend fun scanProduct(
cardId: String? = null,
messageRes: Int? = null,
@ -138,7 +141,9 @@ class TangemSdkManager(
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
return runTaskAsyncReturnOnMain(
runnable = ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository),
runnable = ResetToFactorySettingsTask(
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
),
cardId = cardId,
initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)),
)
@ -243,17 +248,8 @@ class TangemSdkManager(
return resources.getString(stringResId, *formatArgs)
}
fun setAccessCodeRequestPolicy(useBiometricsForAccessCode: Boolean) {
tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) {
UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode)
} else {
UserCodeRequestPolicy.Default
}
}
fun useBiometricsForAccessCode(): Boolean {
val policy = tangemSdk.config.userCodeRequestPolicy
return policy is UserCodeRequestPolicy.AlwaysWithBiometrics && policy.codeType == UserCodeType.AccessCode
fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) {
tangemSdk.config.userCodeRequestPolicy = policy
}
companion object {

View file

@ -21,7 +21,8 @@ sealed class DetailsAction : Action {
sealed class ResetToFactory : DetailsAction() {
object Start : ResetToFactory()
object Proceed : ResetToFactory()
data class Confirm(val confirmed: Boolean) : ResetToFactory()
data class AcceptCondition1(val accepted: Boolean) : ResetToFactory()
data class AcceptCondition2(val accepted: Boolean) : ResetToFactory()
object Failure : ResetToFactory()
object Success : ResetToFactory()
}

View file

@ -1,13 +1,11 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.CompletionResult
import com.tangem.common.*
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -103,6 +101,27 @@ class DetailsMiddleware {
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)) }
@ -126,6 +145,9 @@ class DetailsMiddleware {
Analytics.send(Settings.CardSettings.FactoryResetFinished(error))
}
}
.doOnResult {
doAfterErase()
}
}
}
else -> Unit

View file

@ -93,6 +93,9 @@ private fun handlePrepareCardSettingsScreen(
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
card = card,
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
resetButtonEnabled = false,
condition1Checked = false,
condition2Checked = false,
accessCodeRecovery = if (cardTypesResolver.isWallet2()) {
val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false
AccessCodeRecoveryState(
@ -145,8 +148,25 @@ private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: Card
private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.ResetToFactory.Confirm ->
state.copy(cardSettingsState = state.cardSettingsState?.copy(resetConfirmed = action.confirmed))
is DetailsAction.ResetToFactory.AcceptCondition1 -> {
val warning1Checked = action.accepted
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
condition1Checked = action.accepted,
resetButtonEnabled = warning1Checked && state.cardSettingsState.condition2Checked,
),
)
}
is DetailsAction.ResetToFactory.AcceptCondition2 -> {
val warning2Checked = action.accepted
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
condition2Checked = action.accepted,
resetButtonEnabled = warning2Checked && state.cardSettingsState.condition1Checked,
),
)
}
else -> state
}
}

View file

@ -41,7 +41,9 @@ data class CardSettingsState(
val card: CardDTO,
val manageSecurityState: ManageSecurityState?,
val resetCardAllowed: Boolean,
val resetConfirmed: Boolean = false,
val resetButtonEnabled: Boolean,
val condition1Checked: Boolean,
val condition2Checked: Boolean,
val accessCodeRecovery: AccessCodeRecoveryState? = null,
)

View file

@ -1,5 +1,7 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -33,6 +35,7 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
)
}
@OptIn(ExperimentalAnimationApi::class)
@Suppress("LongMethod", "MagicNumber")
@Composable
private fun ResetCardView(state: ResetCardScreenState) {
@ -76,54 +79,78 @@ private fun ResetCardView(state: ResetCardScreenState) {
)
Spacer(modifier = Modifier.size(28.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { state.onAcceptWarningToggleClick(!state.accepted) },
)
.padding(top = 16.dp, bottom = 16.dp),
) {
IconToggleButton(
checked = state.accepted,
onCheckedChange = state.onAcceptWarningToggleClick,
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
) {
Icon(
painter = painterResource(
if (state.accepted) {
R.drawable.ic_accepted
} else {
R.drawable.ic_unticked
},
),
contentDescription = null,
tint = if (state.accepted) {
TangemTheme.colors.icon.accent
} else {
TangemTheme.colors.icon.secondary
},
)
}
Text(
text = stringResource(id = R.string.reset_card_to_factory_warning_message),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier.padding(end = 20.dp),
)
}
ConditionCheckBox(
checkedState = state.acceptCondition1Checked,
onCheckedChange = state.onAcceptCondition1ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
onCheckedChange = state.onAcceptCondition2ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
)
Spacer(modifier = Modifier.size(16.dp))
Box(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
DetailsMainButton(
title = stringResource(id = R.string.reset_card_to_factory_button_title),
onClick = state.onResetButtonClick,
enabled = state.resetButtonEnabled,
AnimatedContent(
targetState = state.resetButtonEnabled,
label = "Update checked state",
) { buttonEnabled ->
DetailsMainButton(
title = stringResource(id = R.string.reset_card_to_factory_button_title),
onClick = state.onResetButtonClick,
enabled = buttonEnabled,
)
}
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) -> Unit, description: TextReference) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { onCheckedChange.invoke(!checkedState) },
)
.padding(top = TangemTheme.dimens.size16, bottom = TangemTheme.dimens.size16),
) {
IconToggleButton(
checked = checkedState,
onCheckedChange = onCheckedChange,
modifier = Modifier.padding(start = TangemTheme.dimens.size20, end = TangemTheme.dimens.size20),
) {
AnimatedContent(targetState = checkedState, label = "Update checked state") { checked ->
Icon(
painter = painterResource(
if (checked) {
R.drawable.ic_accepted
} else {
R.drawable.ic_unticked
},
),
contentDescription = null,
tint = if (checked) {
TangemTheme.colors.icon.accent
} else {
TangemTheme.colors.icon.secondary
},
)
}
}
Text(
text = description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier.padding(end = TangemTheme.dimens.size20),
)
}
}
@ -138,7 +165,8 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
state = ResetCardScreenState(
accepted = true,
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
onAcceptWarningToggleClick = {},
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},
onResetButtonClick = {},
),
onBackClick = {},

View file

@ -5,7 +5,10 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference
internal data class ResetCardScreenState(
val accepted: Boolean = false,
val descriptionText: TextReference,
val onAcceptWarningToggleClick: (Boolean) -> Unit,
val acceptCondition1Checked: Boolean = false,
val acceptCondition2Checked: Boolean = false,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
) {
val resetButtonEnabled: Boolean

View file

@ -15,9 +15,12 @@ internal class ResetCardViewModel(private val store: Store<AppState>) {
?: TextReference.Str(value = "")
return ResetCardScreenState(
accepted = state?.resetConfirmed ?: false,
accepted = state?.resetButtonEnabled ?: false,
descriptionText = descriptionText,
onAcceptWarningToggleClick = { store.dispatch(DetailsAction.ResetToFactory.Confirm(it)) },
acceptCondition1Checked = state?.condition1Checked ?: false,
acceptCondition2Checked = state?.condition2Checked ?: false,
onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) },
onAcceptCondition2ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition2(it)) },
onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) },
)
}

View file

@ -351,7 +351,6 @@
</plurals>
<string name="registration_task_alert_message">Пожалуйста, удерживайте карту до завершения операции</string>
<string name="reset_card_to_factory_button_title">Сбросить карту</string>
<string name="reset_card_to_factory_warning_message">Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку</string>
<string name="reset_card_with_backup_to_factory_message">Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа.</string>
<string name="reset_card_without_backup_to_factory_message">Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек.</string>
<string name="russian_bank_card_warning_subtitle">У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ?</string>

View file

@ -277,7 +277,6 @@
</plurals>
<string name="registration_task_alert_message">請持有卡片直至操作完成</string>
<string name="reset_card_to_factory_button_title">重置卡片</string>
<string name="reset_card_to_factory_warning_message">我了解執行此操作後,我將無法再訪問當前錢包</string>
<string name="reset_card_with_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼</string>
<string name="reset_card_without_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包</string>
<string name="russian_bank_card_warning_title">目前不接受俄羅斯銀行卡</string>

View file

@ -371,7 +371,8 @@
</plurals>
<string name="registration_task_alert_message">Please hold the card until the operation complete</string>
<string name="reset_card_to_factory_button_title">Reset the Card</string>
<string name="reset_card_to_factory_warning_message">I understand that after performing this action, I will no longer have access to the current wallet</string>
<string name="reset_card_to_factory_condition_1">I realize that I\'ll lose access to my funds on this card after this action.</string>
<string name="reset_card_to_factory_condition_2">I realize that I can\'t use this card to recover my access code on the other cards.</string>
<string name="reset_card_with_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.</string>
<string name="reset_card_without_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.</string>
<string name="russian_bank_card_warning_subtitle">Do you have a bank card from another country and a residence permit or registration outside the Russian Federation?</string>