Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-17 16:39:05 +03:00
parent 587a0c8624
commit 88cf7ec1af
212 changed files with 1223 additions and 1128 deletions

View file

@ -6,6 +6,7 @@ import com.tangem.domain.models.scan.ScanResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
@Suppress("BooleanPropertyNaming")
sealed class DetailsAction : Action {
data class PrepareScreen(
@ -32,7 +33,7 @@ sealed class DetailsAction : Action {
data object EnrollBiometrics : AppSettings()
data class BiometricsStatusChanged(
val needEnrollBiometrics: Boolean,
val isEnrollBiometricsNeeded: Boolean,
) : AppSettings()
data class ChangeAppThemeMode(
@ -40,7 +41,7 @@ sealed class DetailsAction : Action {
) : AppSettings()
data class ChangeBalanceHiding(
val hideBalance: Boolean,
val shouldHideBalance: Boolean,
) : AppSettings()
data class ChangeAppCurrency(

View file

@ -86,7 +86,7 @@ class DetailsMiddleware {
changeAppThemeMode(action.appThemeMode)
}
is DetailsAction.AppSettings.ChangeBalanceHiding -> {
changeBalanceHiding(action.hideBalance)
changeBalanceHiding(action.shouldHideBalance)
}
is DetailsAction.AppSettings.ChangeAppCurrency -> {
store.dispatch(GlobalAction.ChangeAppCurrency(action.currency))
@ -153,9 +153,9 @@ class DetailsMiddleware {
private suspend fun setBiometricLockForAllWallets() {
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val userWallets = userWalletsListRepository.userWalletsSync()
userWallets.forEach {
userWallets.forEach { wallet ->
userWalletsListRepository.setLock(
userWalletId = it.walletId,
userWalletId = wallet.walletId,
lockMethod = LockMethod.Biometric,
changeUnsecured = false,
)
@ -174,11 +174,11 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk)
userWalletsListRepository.userWalletsSync().forEach {
if (it is UserWallet.Hot) {
userWalletsListRepository.userWalletsSync().forEach { wallet ->
if (wallet is UserWallet.Hot) {
userWalletsListRepository.saveWithoutLock(
userWallet = it.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId),
userWallet = wallet.copy(
hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(wallet.hotWalletId),
),
)
}
@ -188,10 +188,10 @@ class DetailsMiddleware {
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {
val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
if (needEnrollBiometrics != null) {
emit(needEnrollBiometrics)
if (isEnrollBiometricsNeeded != null) {
emit(isEnrollBiometricsNeeded)
}
delay(timeMillis = 200)

View file

@ -84,7 +84,7 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
)
is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy(
appSettingsState = state.appSettingsState.copy(
needEnrollBiometrics = action.needEnrollBiometrics,
needEnrollBiometrics = action.isEnrollBiometricsNeeded,
),
)
is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy(
@ -99,7 +99,7 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
)
is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy(
appSettingsState = state.appSettingsState.copy(
isHidingEnabled = action.hideBalance,
isHidingEnabled = action.shouldHideBalance,
),
)
// state should be copied to avoid concurrent modifications from different sources

View file

@ -11,6 +11,7 @@ data class DetailsState(
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
@Suppress("BooleanPropertyNaming")
data class AppSettingsState(
@Deprecated("Delete after hot wallet release")
val saveWallets: Boolean = false,

View file

@ -341,16 +341,16 @@ private class AppCurrencySelectorStateProvider : CollectionPreviewParameterProvi
.mapIndexed { index, s -> Currency(index.toString(), s) }
.toPersistentList()
AppCurrencySelectorState.Loading(onBackClick = {}).let(::add)
AppCurrencySelectorState.Default(
add(AppCurrencySelectorState.Loading(onBackClick = {}))
add(AppCurrencySelectorState.Default(
selectedId = "0",
items = items,
scrollToSelected = consumedEvent(),
onCurrencyClick = {},
onBackClick = {},
onTopBarActionClick = {},
).let(::add)
AppCurrencySelectorState.Search(
))
add(AppCurrencySelectorState.Search(
selectedId = "0",
items = items,
scrollToSelected = consumedEvent(),
@ -358,7 +358,7 @@ private class AppCurrencySelectorStateProvider : CollectionPreviewParameterProvi
onBackClick = {},
onSearchInputChange = {},
onTopBarActionClick = {},
).let(::add)
))
},
)
// endregion Preview

View file

@ -27,16 +27,16 @@ import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
onBackClick = onBackClick,
modifier = modifier,
titleRes = R.string.app_settings_title,
addBottomInsets = false,
content = {
when (state) {
is AppSettingsScreenState.Content -> AppSettings(state = state)
is AppSettingsScreenState.Loading -> Unit
}
},
titleRes = R.string.app_settings_title,
onBackClick = onBackClick,
addBottomInsets = false,
)
}
@ -103,10 +103,10 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
)
AppSettingsScreenState.Content(
add(AppSettingsScreenState.Content(
items = items,
dialog = null,
).let(::add)
))
},
)
// endregion Preview

View file

@ -47,8 +47,8 @@ private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog.Al
collection = buildList {
val dialogsFactory = AppSettingsDialogsFactory()
dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add)
dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add)
add(dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}))
add(dialogsFactory.createDeleteSavedWalletsAlert({}, {}))
},
)
// endregion Preview

View file

@ -64,10 +64,10 @@ private class ButtonItemProvider : CollectionPreviewParameterProvider<Item.Butto
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createSelectAppCurrencyButton(
add(itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = "US Dollar",
onClick = { /* no-op */ },
).let(::add)
))
},
)
// endregion Preview

View file

@ -71,9 +71,9 @@ private class CardItemProvider : CollectionPreviewParameterProvider<Item.Card>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createEnrollBiometricsCard(
add(itemsFactory.createEnrollBiometricsCard(
onClick = { /* no-op */ },
).let(::add)
))
},
)
// endregion Preview

View file

@ -85,26 +85,26 @@ private class SwitchItemProvider : CollectionPreviewParameterProvider<Item.Switc
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createSaveAccessCodeSwitch(
add(itemsFactory.createSaveAccessCodeSwitch(
isChecked = true,
isEnabled = true,
onCheckedChange = { /* no-op */ },
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
))
add(itemsFactory.createSaveAccessCodeSwitch(
isChecked = false,
isEnabled = true,
onCheckedChange = { /* no-op */ },
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
))
add(itemsFactory.createSaveAccessCodeSwitch(
isChecked = true,
isEnabled = false,
onCheckedChange = { /* no-op */ },
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
))
add(itemsFactory.createSaveAccessCodeSwitch(
isChecked = false,
isEnabled = false,
onCheckedChange = { /* no-op */ },
).let(::add)
))
},
)
// endregion Preview

View file

@ -99,60 +99,76 @@ internal class AppSettingsModel @Inject constructor(
}
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
val items = buildList {
val items = buildList<AppSettingsScreenState.Item> {
if (state.needEnrollBiometrics) {
itemsFactory.createEnrollBiometricsCard(
onClick = ::enrollBiometrics,
).let(::add)
add(
itemsFactory.createEnrollBiometricsCard(
onClick = ::enrollBiometrics,
),
)
}
itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = state.selectedAppCurrency.name,
onClick = ::showAppCurrencySelector,
).let(::add)
add(
itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = state.selectedAppCurrency.name,
onClick = ::showAppCurrencySelector,
),
)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics,
onCheckedChange = ::onBiometricAuthenticationToggled,
).let(::add)
add(
itemsFactory.createUseBiometricsSwitch(
isChecked = state.useBiometricAuthentication,
isEnabled = canUseBiometrics,
onCheckedChange = ::onBiometricAuthenticationToggled,
),
)
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled,
).let(::add)
add(
itemsFactory.createRequireAccessCodeSwitch(
isChecked = state.requireAccessCode,
isEnabled = canUseBiometrics && state.useBiometricAuthentication,
onCheckedChange = ::onRequireAccessCodeToggled,
),
)
} else {
if (state.isBiometricsAvailable) {
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
).let(::add)
add(
itemsFactory.createSaveWalletsSwitch(
isChecked = state.saveWallets,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveWalletsToggled,
),
)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
).let(::add)
add(
itemsFactory.createSaveAccessCodeSwitch(
isChecked = state.saveAccessCodes,
isEnabled = canUseBiometrics,
onCheckedChange = ::onSaveAccessCodesToggled,
),
)
}
}
itemsFactory.createFlipToHideBalanceSwitch(
isChecked = state.isHidingEnabled,
isEnabled = true,
onCheckedChange = ::onFlipToHideBalanceToggled,
).let(::add)
add(
itemsFactory.createFlipToHideBalanceSwitch(
isChecked = state.isHidingEnabled,
isEnabled = true,
onCheckedChange = ::onFlipToHideBalanceToggled,
),
)
itemsFactory.createSelectThemeModeButton(
currentThemeMode = state.selectedThemeMode,
onClick = { showThemeModeSelector(state.selectedThemeMode) },
).let(::add)
add(
itemsFactory.createSelectThemeModeButton(
currentThemeMode = state.selectedThemeMode,
onClick = { showThemeModeSelector(state.selectedThemeMode) },
),
)
}
return items.toImmutableList()
@ -280,7 +296,7 @@ internal class AppSettingsModel @Inject constructor(
val param = AnalyticsParam.OnOffState(enable)
analyticsEventHandler.send(Settings.AppSettings.HideBalanceChanged(param))
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(hideBalance = enable))
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(shouldHideBalance = enable))
}
private fun dismissDialog() {
@ -290,10 +306,10 @@ internal class AppSettingsModel @Inject constructor(
private fun bootstrapAppCurrencyUpdates() {
appCurrencyRepository
.getSelectedAppCurrency()
.onEach {
if (it.code == store.state.globalState.appCurrency.code) return@onEach
.onEach { appCurrency ->
if (appCurrency.code == store.state.globalState.appCurrency.code) return@onEach
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(it))
store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(appCurrency))
}
.launchIn(scope)
.saveIn(appCurrencyUpdatesJobHolder)

View file

@ -26,19 +26,19 @@ import com.tangem.wallet.R
@Composable
internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) {
val needReadCard = state.cardDetails == null
val isCardReadingNeeded = state.cardDetails == null
SettingsScreensScaffold(
onBackClick = state.onBackClick,
modifier = modifier,
titleRes = R.string.card_settings_title,
content = {
if (needReadCard) {
if (isCardReadingNeeded) {
CardSettingsReadCard(state.onScanCardClick)
} else {
CardSettings(state = state)
}
},
titleRes = R.string.card_settings_title,
onBackClick = state.onBackClick,
)
}
@ -123,8 +123,8 @@ private fun CardSettings(state: CardSettingsScreenState) {
.fillMaxWidth()
.testTag(DeviceSettingsScreenTestTags.LAZY_LIST),
) {
items(state.cardDetails) {
val paddingBottom = when (it) {
items(state.cardDetails) { cardInfo ->
val paddingBottom = when (cardInfo) {
is CardInfo.CardId, is CardInfo.Issuer -> TangemTheme.dimens.spacing12
is CardInfo.SignedHashes -> TangemTheme.dimens.spacing14
is CardInfo.SecurityMode -> TangemTheme.dimens.spacing16
@ -132,7 +132,7 @@ private fun CardSettings(state: CardSettingsScreenState) {
is CardInfo.AccessCodeRecovery -> TangemTheme.dimens.spacing16
is CardInfo.ResetToFactorySettings -> TangemTheme.dimens.spacing28
}
val paddingTop = when (it) {
val paddingTop = when (cardInfo) {
is CardInfo.CardId -> TangemTheme.dimens.spacing0
is CardInfo.Issuer -> TangemTheme.dimens.spacing12
is CardInfo.SignedHashes -> TangemTheme.dimens.spacing12
@ -145,8 +145,8 @@ private fun CardSettings(state: CardSettingsScreenState) {
modifier = Modifier
.fillMaxWidth()
.clickable(
enabled = it.clickable,
onClick = { state.onElementClick(it) },
enabled = cardInfo.isClickable,
onClick = { state.onElementClick(cardInfo) },
)
.padding(
start = TangemTheme.dimens.spacing20,
@ -155,25 +155,25 @@ private fun CardSettings(state: CardSettingsScreenState) {
top = paddingTop,
),
) {
val titleColor = if (it.clickable) {
val titleColor = if (cardInfo.isClickable) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.tertiary
}
val subtitleColor = if (it.clickable) {
val subtitleColor = if (cardInfo.isClickable) {
TangemTheme.colors.text.secondary
} else {
TangemTheme.colors.text.tertiary
}
Text(
text = it.titleRes.resolveReference(),
text = cardInfo.titleRes.resolveReference(),
color = titleColor,
style = TangemTheme.typography.subtitle1,
modifier = Modifier.testTag(DeviceSettingsScreenTestTags.ITEM_TITLE),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size4))
Text(
text = it.subtitle.resolveReference(),
text = cardInfo.subtitle.resolveReference(),
color = subtitleColor,
style = TangemTheme.typography.body2,
modifier = Modifier.testTag(DeviceSettingsScreenTestTags.ITEM_SUBTITLE),

View file

@ -18,7 +18,7 @@ internal data class CardSettingsScreenState(
internal sealed class CardInfo(
val titleRes: TextReference,
val subtitle: TextReference,
val clickable: Boolean = false,
val isClickable: Boolean = false,
) {
class CardId(subtitle: String) : CardInfo(
titleRes = TextReference.Res(R.string.details_row_title_cid),
@ -38,29 +38,29 @@ internal sealed class CardInfo(
class SecurityMode(securityOption: SecurityOption, clickable: Boolean) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_security_mode),
subtitle = TextReference.Res(securityOption.toTitleRes()),
clickable = clickable,
isClickable = clickable,
)
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,
isClickable = true,
)
class AccessCodeRecovery(val enabled: Boolean) : CardInfo(
class AccessCodeRecovery(val isEnabled: Boolean) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title),
subtitle = if (enabled) {
subtitle = if (isEnabled) {
TextReference.Res(R.string.common_enabled)
} else {
TextReference.Res(R.string.common_disabled)
},
clickable = true,
isClickable = true,
)
class ResetToFactorySettings(description: TextReference) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
subtitle = description,
clickable = true,
isClickable = true,
)
}

View file

@ -16,8 +16,8 @@ import com.tangem.wallet.R
@Composable
fun AccessCodeRecoveryScreen(state: AccessCodeRecoveryScreenState, onBackClick: () -> Unit) {
SettingsScreensScaffold(
content = { AccessCodeRecoveryOptions(state = state) },
onBackClick = onBackClick,
content = { AccessCodeRecoveryOptions(state = state) },
)
}
@ -38,13 +38,13 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
DetailsRadioButtonElement(
title = stringResourceSafe(id = R.string.common_enabled),
subtitle = stringResourceSafe(id = R.string.card_settings_access_code_recovery_enabled_description),
selected = state.enabledSelection,
isSelected = state.isEnabledSelection,
onClick = { state.onOptionClick(true) },
)
DetailsRadioButtonElement(
title = stringResourceSafe(id = R.string.common_disabled),
subtitle = stringResourceSafe(id = R.string.card_settings_access_code_recovery_disabled_description),
selected = !state.enabledSelection,
isSelected = !state.isEnabledSelection,
onClick = { state.onOptionClick(false) },
)

View file

@ -1,15 +1,15 @@
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 isEnabledOnCard Indicates whether access code recovery is enabled on the card
* @property isEnabledSelection 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 isEnabledOnCard: Boolean,
val isEnabledSelection: Boolean,
val isSaveChangesEnabled: Boolean,
val onSaveChangesClick: () -> Unit,
val onOptionClick: (Boolean) -> Unit,

View file

@ -43,8 +43,8 @@ internal class AccessCodeRecoveryModel @Inject constructor(
)
return AccessCodeRecoveryScreenState(
enabledOnCard = isEnabled,
enabledSelection = isEnabled,
isEnabledOnCard = isEnabled,
isEnabledSelection = isEnabled,
isSaveChangesEnabled = false,
onSaveChangesClick = ::saveChanges,
onOptionClick = ::selectOption,
@ -52,7 +52,7 @@ internal class AccessCodeRecoveryModel @Inject constructor(
}
private fun saveChanges() = modelScope.launch {
val isEnabled = screenState.value.enabledSelection
val isEnabled = screenState.value.isEnabledSelection
tangemSdkManager
.setAccessCodeRecoveryEnabled(scannedScanResponse.card.cardId, isEnabled)
@ -78,10 +78,10 @@ internal class AccessCodeRecoveryModel @Inject constructor(
}
private fun selectOption(isEnabled: Boolean) {
screenState.update {
it.copy(
enabledSelection = isEnabled,
isSaveChangesEnabled = isEnabled != it.enabledOnCard,
screenState.update { state ->
state.copy(
isEnabledSelection = isEnabled,
isSaveChangesEnabled = isEnabled != state.isEnabledOnCard,
)
}
}

View file

@ -23,9 +23,9 @@ internal class CardSettingsInteractor @Inject constructor() {
}
fun update(transform: (ScanResponse) -> ScanResponse) {
_scannedScanResponse.update {
requireNotNull(it)
transform(it)
_scannedScanResponse.update { scanResponse ->
requireNotNull(scanResponse)
transform(scanResponse)
}
}

View file

@ -57,7 +57,7 @@ internal class CardSettingsModel @Inject constructor(
private val params = paramsContainer.require<CardSettingsComponent.Params>()
private var previousBiometricsRequestPolicy: Boolean = false
private var isBiometricsRequestPolicyPrevious: Boolean = false
private val userWalletId = params.userWalletId
@ -77,13 +77,13 @@ internal class CardSettingsModel @Inject constructor(
// Reset card scanned data
cardSettingsInteractor.clear()
// Restore the previous value of access code request policy
cardSdkConfigRepository.isBiometricsRequestPolicy = previousBiometricsRequestPolicy
cardSdkConfigRepository.isBiometricsRequestPolicy = isBiometricsRequestPolicyPrevious
}
private fun updateAccessCodeRequestPolicy() {
runBlocking {
// !!!IMPORTANT!!!: Do not forget to restore the previous value in onCleared() method
previousBiometricsRequestPolicy = cardSdkConfigRepository.isBiometricsRequestPolicy
isBiometricsRequestPolicyPrevious = cardSdkConfigRepository.isBiometricsRequestPolicy
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet $userWalletId not found") }
@ -135,34 +135,38 @@ internal class CardSettingsModel @Inject constructor(
)
val isResetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver)
val cardDetails = buildList {
CardInfo.CardId(cardId).let(::add)
CardInfo.Issuer(card.issuer.name).let(::add)
val cardDetails = buildList<CardInfo> {
add(CardInfo.CardId(cardId))
add(CardInfo.Issuer(card.issuer.name))
if (!cardTypesResolver.isTangemTwins()) {
CardInfo.SignedHashes(card.signedHashesCount().toString()).let(::add)
add(CardInfo.SignedHashes(card.signedHashesCount().toString()))
}
CardInfo.SecurityMode(
currentSecurityOption,
clickable = allowedSecurityOptions.size > 1,
).let(::add)
add(
CardInfo.SecurityMode(
currentSecurityOption,
clickable = allowedSecurityOptions.size > 1,
),
)
if (card.backupStatus?.isActive == true && card.isAccessCodeSet) {
CardInfo.ChangeAccessCode.let(::add)
add(CardInfo.ChangeAccessCode)
}
if (isAccessCodeRecoveryAllowed(cardTypesResolver)) {
CardInfo.AccessCodeRecovery(isAccessCodeRecoveryEnabled(cardTypesResolver, card)).let(::add)
add(CardInfo.AccessCodeRecovery(isAccessCodeRecoveryEnabled(cardTypesResolver, card)))
}
if (isResetCardAllowed) {
CardInfo.ResetToFactorySettings(
description = getResetToFactoryDescription(
isActiveBackupStatus = card.backupStatus?.isActive == true,
typesResolver = cardTypesResolver,
add(
CardInfo.ResetToFactorySettings(
description = getResetToFactoryDescription(
isActiveBackupStatus = card.backupStatus?.isActive == true,
typesResolver = cardTypesResolver,
),
),
).let(::add)
)
}
}

View file

@ -19,11 +19,11 @@ import com.tangem.wallet.R
@Composable
internal fun SettingsScreensScaffold(
onBackClick: () -> Unit,
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
@StringRes titleRes: Int? = null,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
addBottomInsets: Boolean = true,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable () -> Unit,
fab: @Composable () -> Unit = {},
) {
val backgroundColor = TangemTheme.colors.background.secondary
@ -129,18 +129,18 @@ internal fun DetailsMainButton(
}
@Composable
internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
internal fun DetailsRadioButtonElement(title: String, subtitle: String, isSelected: Boolean, onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = selected,
selected = isSelected,
onClick = { onClick() },
)
.padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
) {
RadioButton(
selected = selected,
selected = isSelected,
onClick = null,
modifier = Modifier.padding(end = 20.dp),
colors = RadioButtonDefaults.colors(

View file

@ -7,7 +7,7 @@ internal fun isAccessCodeRecoveryAllowed(typeResolver: CardTypesResolver): Boole
internal fun isAccessCodeRecoveryEnabled(typeResolver: CardTypesResolver, card: CardDTO): Boolean =
if (typeResolver.isWallet2()) {
card.userSettings?.isUserCodeRecoveryAllowed ?: false
card.userSettings?.isUserCodeRecoveryAllowed == true
} else {
false
}

View file

@ -27,11 +27,11 @@ import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog
@Composable
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
onBackClick = onBackClick,
modifier = modifier,
content = {
ResetCardView(state = state)
},
onBackClick = onBackClick,
)
when (val dialog = state.dialog) {
@ -65,7 +65,7 @@ private fun ResetCardView(state: ResetCardScreenState) {
Conditions(state)
DynamicSpacer(scrollState = scrollState)
SpacerH16()
ResetButton(enabled = state.resetButtonEnabled, onResetButtonClick = state.onResetButtonClick)
ResetButton(enabled = state.isResetButtonEnabled, onResetButtonClick = state.onResetButtonClick)
SpacerH16()
}
}
@ -113,18 +113,18 @@ private fun Description(text: TextReference) {
@Composable
private fun Conditions(state: ResetCardScreenState) {
state.warningsToShow.forEach {
when (it) {
state.warningsToShow.forEach { warning ->
when (warning) {
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
ConditionCheckBox(
checkedState = state.acceptCondition1Checked,
checkedState = state.isAcceptCondition1Checked,
onCheckedChange = state.onAcceptCondition1ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
}
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
checkedState = state.isAcceptCondition2Checked,
onCheckedChange = state.onAcceptCondition2ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
)
@ -238,12 +238,12 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
) {
ResetCardScreen(
state = ResetCardScreenState(
resetButtonEnabled = true,
showResetPasswordButton = false,
isResetButtonEnabled = true,
isResetPasswordButtonShown = false,
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
acceptCondition1Checked = false,
acceptCondition2Checked = false,
isAcceptCondition1Checked = false,
isAcceptCondition2Checked = false,
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},
onResetButtonClick = {},

View file

@ -5,12 +5,12 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal data class ResetCardScreenState(
val resetButtonEnabled: Boolean,
val isResetButtonEnabled: Boolean,
val descriptionText: TextReference,
val warningsToShow: List<WarningsToReset>,
val showResetPasswordButton: Boolean,
val acceptCondition1Checked: Boolean,
val acceptCondition2Checked: Boolean,
val isResetPasswordButtonShown: Boolean,
val isAcceptCondition1Checked: Boolean,
val isAcceptCondition2Checked: Boolean,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,

View file

@ -97,15 +97,15 @@ internal class ResetCardModel @Inject constructor(
}
return ResetCardScreenState(
resetButtonEnabled = false,
isResetButtonEnabled = false,
descriptionText = getResetToFactoryDescription(
isActiveBackupStatus = isActiveBackupPrimaryCard,
typesResolver = currentCardTypesResolver,
),
warningsToShow = warningsToShow,
showResetPasswordButton = shouldShowResetPasswordButton,
acceptCondition1Checked = false,
acceptCondition2Checked = false,
isResetPasswordButtonShown = shouldShowResetPasswordButton,
isAcceptCondition1Checked = false,
isAcceptCondition2Checked = false,
onAcceptCondition1ToggleClick = ::toggleFirstCondition,
onAcceptCondition2ToggleClick = ::toggleSecondCondition,
onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) },
@ -121,26 +121,26 @@ internal class ResetCardModel @Inject constructor(
private fun toggleFirstCondition(isAccepted: Boolean) {
screenState.update { prevState ->
val resetButtonEnabled = if (prevState.showResetPasswordButton) {
isAccepted && prevState.acceptCondition2Checked
val isResetButtonEnabled = if (prevState.isResetPasswordButtonShown) {
isAccepted && prevState.isAcceptCondition2Checked
} else {
isAccepted
}
prevState.copy(
acceptCondition1Checked = isAccepted,
resetButtonEnabled = resetButtonEnabled,
isAcceptCondition1Checked = isAccepted,
isResetButtonEnabled = isResetButtonEnabled,
)
}
}
private fun toggleSecondCondition(isAccepted: Boolean) {
screenState.update { prevState ->
val resetButtonEnabled = prevState.acceptCondition1Checked && isAccepted
val isResetButtonEnabled = prevState.isAcceptCondition1Checked && isAccepted
prevState.copy(
acceptCondition2Checked = isAccepted,
resetButtonEnabled = resetButtonEnabled,
isAcceptCondition2Checked = isAccepted,
isResetButtonEnabled = isResetButtonEnabled,
)
}
}
@ -183,14 +183,14 @@ internal class ResetCardModel @Inject constructor(
modelScope.launch {
resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight {
deleteSavedAccessCodesUseCase(cardId = primaryCardId)
val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse {
Timber.e("Unable to delete user wallet: $it")
val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse { error ->
Timber.e("Unable to delete user wallet: $error")
return@launch
}
if (hasUserWallets) {
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrElse {
error("Failed to get selected wallet: $it")
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrElse { error ->
error("Failed to get selected wallet: $error")
}
store.onUserWalletSelected(newSelectedWallet)
@ -269,7 +269,7 @@ internal class ResetCardModel @Inject constructor(
if (hotWalletFeatureToggles.isHotWalletEnabled) {
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
} else {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLocked }.isSuccess
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else {

View file

@ -22,10 +22,10 @@ internal fun SecurityModeScreen(
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
onBackClick = onBackClick,
modifier = modifier,
content = { SecurityModeOptions(state = state) },
// titleRes = R.string.card_settings_security_mode,
onBackClick = onBackClick,
)
}
@ -57,7 +57,7 @@ private fun SecurityModeOptions(state: SecurityModeScreenState) {
@Composable
private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
val selected = option == state.selectedSecurityMode
val isSelected = option == state.selectedSecurityMode
val title = option.toTitleRes()
@ -70,7 +70,7 @@ private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenStat
DetailsRadioButtonElement(
title = stringResourceSafe(id = title),
subtitle = stringResourceSafe(id = subtitle),
selected = selected,
isSelected = isSelected,
onClick = { state.onNewModeSelected(option) },
)
}

View file

@ -77,9 +77,9 @@ internal class SecurityModeModel @Inject constructor(
SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
}
cardSettingsInteractor.update {
it.copy(
card = it.card.copy(
cardSettingsInteractor.update { scanResponse ->
scanResponse.copy(
card = scanResponse.card.copy(
isAccessCodeSet = selectedOption == SecurityOption.AccessCode,
isPasscodeSet = selectedOption == SecurityOption.PassCode,
),

View file

@ -150,7 +150,7 @@ internal class MainViewModel @Inject constructor(
prepareSelectedWalletFeedback()
// await while initial route stack is initialized
appRouterConfig.isInitialized.first { it }
appRouterConfig.initializedState.first { it }
isSplashScreenShown = false
}
@ -179,11 +179,9 @@ internal class MainViewModel @Inject constructor(
private fun prepareSelectedWalletFeedback() {
getSelectedWalletUseCase.invoke()
.mapLeft { emptyFlow<UserWallet>() }
.onRight {
it.distinctUntilChanged()
.onEach { userWallet ->
Analytics.setContext(userWallet)
}
.onRight { wallet ->
wallet.distinctUntilChanged()
.onEach { Analytics.setContext(it) }
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
@ -208,7 +206,7 @@ internal class MainViewModel @Inject constructor(
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
logEnabled = LogConfig.network.moonPayService,
isLogEnabled = LogConfig.network.moonPayService,
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}
@ -224,8 +222,8 @@ internal class MainViewModel @Inject constructor(
.filter {
it.isBalanceHidingNotificationEnabled && it.isBalanceHidden
}
.onEach {
if (!it.isUpdateFromToast) {
.onEach { settings ->
if (!settings.isUpdateFromToast) {
listenToFlipsUseCase.changeUpdateEnabled(false)
val message = BottomSheetMessage.invoke(
@ -354,6 +352,7 @@ internal class MainViewModel @Inject constructor(
listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true)
}
@Suppress("NullableToStringCall")
private fun sendKeyboardIdentifierEvent() {
viewModelScope.launch {
val keyboardId = keyboardValidator.getKeyboardId()

View file

@ -34,11 +34,11 @@ object OnboardingHelper {
}
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardRepository.isActivationInProgress(cardId)
val areWalletsEmpty = response.card.wallets.isEmpty()
val isActivationInProgress = cardRepository.isActivationInProgress(cardId)
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
!DemoHelper.isDemoCard(response)
emptyWallets || activationInProgress || isNoBackup
areWalletsEmpty || isActivationInProgress || isNoBackup
}
response.card.wallets.isNotEmpty() -> cardRepository.isActivationInProgress(cardId)

View file

@ -50,8 +50,8 @@ object TradeCryptoMiddleware {
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)?.let {
store.dispatchOpenUrl(it)
)?.let { url ->
store.dispatchOpenUrl(url)
Analytics.send(Token.Withdraw.ScreenOpened())
}
}

View file

@ -72,8 +72,8 @@ internal class WelcomeModel @Inject constructor(
this.state.update { prevState ->
prevState.copy(
showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress,
showUnlockWithCardProgress = state.isUnlockWithCardInProgress,
isUnlockWithBiometricsProgressVisible = state.isUnlockWithBiometricsInProgress,
isUnlockWithCardProgressVisible = state.isUnlockWithCardInProgress,
warning = warning,
error = state.error
?.takeIf { !it.silent && warning == null }

View file

@ -13,7 +13,7 @@ internal sealed interface WelcomeAction : Action {
object ProceedWithCard : WelcomeAction {
object Success : WelcomeAction
data class Error(val error: TangemError) : WelcomeAction
data class ChangeProgress(val showProgress: Boolean) : WelcomeAction
data class ChangeProgress(val isProgress: Boolean) : WelcomeAction
}
object CloseError : WelcomeAction

View file

@ -141,13 +141,13 @@ internal class WelcomeMiddleware {
onSuccess = { scanResponse ->
scope.launch { onCardScanned(scanResponse) }
},
onFailure = {
when (it) {
onFailure = { error ->
when (error) {
is TangemSdkError.ExceptionError -> {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
}
else -> {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(it))
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error))
}
}
},

View file

@ -25,7 +25,7 @@ internal object WelcomeReducer {
isUnlockWithCardInProgress = false,
)
is WelcomeAction.ProceedWithCard.ChangeProgress -> state.copy(
isUnlockWithCardInProgress = action.showProgress,
isUnlockWithCardInProgress = action.isProgress,
)
is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false)
is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false)

View file

@ -5,8 +5,8 @@ import com.tangem.tap.features.welcome.ui.model.WarningModel
internal data class WelcomeScreenState(
val onPopBack: () -> Unit = {},
val showUnlockWithBiometricsProgress: Boolean = false,
val showUnlockWithCardProgress: Boolean = false,
val isUnlockWithBiometricsProgressVisible: Boolean = false,
val isUnlockWithCardProgressVisible: Boolean = false,
val warning: WarningModel? = null,
val error: TextReference? = null,
val onUnlockClick: () -> Unit = {},

View file

@ -39,8 +39,8 @@ internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modif
.systemBarsPadding(),
) {
WelcomeScreenContent(
showUnlockProgress = state.showUnlockWithBiometricsProgress,
showScanCardProgress = state.showUnlockWithCardProgress,
showUnlockProgress = state.isUnlockWithBiometricsProgressVisible,
showScanCardProgress = state.isUnlockWithCardProgressVisible,
onUnlockClick = state.onUnlockClick,
onScanCardClick = state.onScanCardClick,
)
@ -57,8 +57,8 @@ internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modif
WarningDialog(warning)
LaunchedEffect(errorMessage, state.onCloseError) {
errorMessage?.let {
snackbarHostState.showSnackbar(it)
errorMessage?.let { message ->
snackbarHostState.showSnackbar(message)
state.onCloseError()
}
}
@ -82,8 +82,8 @@ private class WelcomeComponentPreviewProvider : PreviewParameterProvider<Welcome
PreviewWelcomeComponent(),
PreviewWelcomeComponent(
initialState = WelcomeScreenState(
showUnlockWithBiometricsProgress = true,
showUnlockWithCardProgress = true,
isUnlockWithBiometricsProgressVisible = true,
isUnlockWithCardProgressVisible = true,
),
),
PreviewWelcomeComponent(