Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-09 20:37:33 +03:00
commit 8e42589a00
456 changed files with 6580 additions and 3738 deletions

View file

@ -59,7 +59,6 @@ class WalletConnectMiddleware {
is WalletConnectAction.StartWalletConnect -> {
val uri = action.copiedUri
if (uri != null && isWalletConnectUri(uri)) {
// TODO check
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) }

View file

@ -18,7 +18,6 @@ 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
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -27,8 +26,9 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
@ -114,7 +114,7 @@ private fun TopBar(
is AppCurrencySelectorState.Loading,
is AppCurrencySelectorState.Default,
-> Text(
text = stringResource(id = R.string.details_row_title_currency),
text = stringResourceSafe(id = R.string.details_row_title_currency),
style = TangemTheme.typography.subtitle1,
)
}
@ -163,7 +163,7 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi
singleLine = true,
textStyle = TangemTheme.typography.subtitle2,
placeholder = {
Text(text = stringResource(id = R.string.common_search))
Text(text = stringResourceSafe(id = R.string.common_search))
},
colors = SearchBarColors,
)

View file

@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.components
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
@ -26,7 +26,7 @@ internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
onClick = dialog.onConfirm,
),
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
title = stringResourceSafe(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
onDismissDialog = dialog.onDismiss,

View file

@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.components
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
@ -22,7 +22,7 @@ internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_cancel),
title = stringResourceSafe(R.string.common_cancel),
onClick = dialog.onDismiss,
),
onSelect = dialog.onSelect,

View file

@ -14,8 +14,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.common.DetailsMainButton
@ -88,13 +88,13 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
),
) {
Text(
text = stringResource(id = R.string.scan_card_settings_title),
text = stringResourceSafe(id = R.string.scan_card_settings_title),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size20))
Text(
text = stringResource(id = R.string.scan_card_settings_message),
text = stringResourceSafe(id = R.string.scan_card_settings_message),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body1,
modifier = Modifier
@ -103,7 +103,7 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
DetailsMainButton(
title = stringResource(id = R.string.scan_card_settings_button),
title = stringResourceSafe(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
)
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.cardsettings
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.wallet.R
@ -77,7 +77,7 @@ internal sealed interface TextReference {
@ReadOnlyComposable
internal fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
is TextReference.Res -> stringResourceSafe(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value
}
}

View file

@ -5,7 +5,7 @@ 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.extensions.stringResourceSafe
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
@ -36,14 +36,14 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
)
DetailsRadioButtonElement(
title = stringResource(id = R.string.common_enabled),
subtitle = stringResource(id = R.string.card_settings_access_code_recovery_enabled_description),
title = stringResourceSafe(id = R.string.common_enabled),
subtitle = stringResourceSafe(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),
title = stringResourceSafe(id = R.string.common_disabled),
subtitle = stringResourceSafe(id = R.string.card_settings_access_code_recovery_disabled_description),
selected = !state.enabledSelection,
onClick = { state.onOptionClick(false) },
)
@ -51,7 +51,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
Spacer(modifier = Modifier.weight(1f))
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
title = stringResourceSafe(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = { state.onSaveChangesClick() },
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),

View file

@ -10,9 +10,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.wallet.R
@ -64,7 +64,7 @@ internal fun SettingsScreensScaffold(
) {
if (titleRes != null) {
Text(
text = stringResource(id = titleRes),
text = stringResourceSafe(id = titleRes),
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing20)
.padding(bottom = TangemTheme.dimens.spacing36),
@ -82,7 +82,7 @@ internal fun SettingsScreensScaffold(
@Composable
internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
Text(
text = stringResource(id = titleRes),
text = stringResourceSafe(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,

View file

@ -10,9 +10,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.cardsettings.TextReference
@ -71,7 +71,7 @@ private fun ResetCardView(state: ResetCardScreenState) {
@Composable
private fun Title() {
Text(
text = stringResource(id = R.string.card_settings_reset_card_to_factory),
text = stringResourceSafe(id = R.string.card_settings_reset_card_to_factory),
style = TangemTheme.typography.h1,
color = TangemTheme.colors.text.primary1,
)
@ -89,7 +89,7 @@ private fun AlertImage() {
@Composable
private fun Subtitle() {
Text(
text = stringResource(id = R.string.common_attention),
text = stringResourceSafe(id = R.string.common_attention),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
@ -179,7 +179,7 @@ private fun ColumnScope.DynamicSpacer(scrollState: ScrollState) {
@Composable
private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
DetailsMainButton(
title = stringResource(id = R.string.reset_card_to_factory_button_title),
title = stringResourceSafe(id = R.string.reset_card_to_factory_button_title),
onClick = onResetButtonClick,
enabled = enabled,
)
@ -188,14 +188,14 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
@Composable
private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
BasicDialog(
title = stringResource(dialog.titleResId),
message = stringResource(dialog.messageResId),
title = stringResourceSafe(dialog.titleResId),
message = stringResourceSafe(dialog.messageResId),
dismissButton = DialogButtonUM(
title = stringResource(id = R.string.common_cancel),
title = stringResourceSafe(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.card_settings_action_sheet_reset),
title = stringResourceSafe(id = R.string.card_settings_action_sheet_reset),
warning = true,
onClick = dialog.onConfirmClick,
),
@ -206,10 +206,10 @@ private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
@Composable
private fun CompletedResetDialog(dialog: ResetCardDialog) {
BasicDialog(
title = stringResource(id = dialog.titleResId),
message = stringResource(id = dialog.messageResId),
title = stringResourceSafe(id = dialog.titleResId),
message = stringResourceSafe(id = dialog.messageResId),
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
title = stringResourceSafe(id = R.string.common_ok),
onClick = dialog.onConfirmClick,
),
onDismissDialog = {},

View file

@ -5,9 +5,9 @@ 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 androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.stringResourceSafe
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
@ -47,7 +47,7 @@ private fun SecurityModeOptions(state: SecurityModeScreenState) {
Spacer(modifier = Modifier.weight(1f))
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
title = stringResourceSafe(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = state.onSaveChangesClicked,
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
@ -68,8 +68,8 @@ private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenStat
}
DetailsRadioButtonElement(
title = stringResource(id = title),
subtitle = stringResource(id = subtitle),
title = stringResourceSafe(id = title),
subtitle = stringResourceSafe(id = subtitle),
selected = selected,
onClick = { state.onNewModeSelected(option) },
)
@ -80,7 +80,7 @@ private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenStat
private fun SecurityModeScreenPreview() {
SecurityModeScreen(
state = SecurityModeScreenState(
availableOptions = SecurityOption.values().toList(),
availableOptions = SecurityOption.entries,
selectedSecurityMode = SecurityOption.LongTap,
isSaveChangesEnabled = false,
onNewModeSelected = {},

View file

@ -5,21 +5,19 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import kotlinx.collections.immutable.persistentListOf
@ -30,8 +28,6 @@ internal fun WalletConnectScreen(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
SettingsScreensScaffold(
modifier = modifier,
content = {
@ -46,7 +42,7 @@ internal fun WalletConnectScreen(
AddSessionFab(
onAddSession = {
Analytics.send(Settings.ButtonStartWalletConnectSession())
state.onAddSession(context.getFromClipboard()?.toString())
state.onAddSession()
},
)
}
@ -60,7 +56,7 @@ internal fun WalletConnectScreen(
private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) {
FloatingActionButton(
onClick = onAddSession,
backgroundColor = TangemTheme.colors.button.primary,
containerColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.icon.primary2,
shape = RoundedCornerShape(16.dp),
modifier = modifier,
@ -96,7 +92,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(id = R.string.wallet_connect_subtitle),
text = stringResourceSafe(id = R.string.wallet_connect_subtitle),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)

View file

@ -6,7 +6,7 @@ internal data class WalletConnectScreenState(
val sessions: ImmutableList<WcSessionForScreen>,
val isLoading: Boolean = false,
val onRemoveSession: (String) -> Unit = {},
val onAddSession: (String?) -> Unit = {},
val onAddSession: () -> Unit = {},
)
data class WcSessionForScreen(

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect
import androidx.lifecycle.*
import arrow.core.getOrElse
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
@ -17,6 +18,7 @@ import javax.inject.Inject
@HiltViewModel
internal class WalletConnectViewModel @Inject constructor(
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val clipboardManager: ClipboardManager,
) : ViewModel(), DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
@ -35,7 +37,11 @@ internal class WalletConnectViewModel @Inject constructor(
sessions.toImmutableList(),
isLoading = state.loading,
onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) },
onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) },
onAddSession = {
store.dispatch(
WalletConnectAction.StartWalletConnect(copiedUri = clipboardManager.getText()),
)
},
)
}

View file

@ -57,6 +57,7 @@ internal fun StoriesScreen(
}
}
// todo refactor [REDACTED_TASK_KEY]
StoriesScreenContent(
modifier = Modifier
.fillMaxSize()
@ -75,6 +76,7 @@ internal fun StoriesScreen(
)
}
@Deprecated("Use StoriesContainer from core/ui")
@Suppress("LongMethod")
@Composable
private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) {

View file

@ -2,20 +2,23 @@ package com.tangem.tap.features.home.compose.content
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
@ -27,8 +30,8 @@ fun StoriesRevolutionaryWallet() {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description),
titleText = stringResourceSafe(id = R.string.story_awe_title),
subtitleText = stringResourceSafe(id = R.string.story_awe_description),
)
},
bottomContent = {
@ -46,8 +49,8 @@ fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = stringResource(id = R.string.story_backup_description),
titleText = stringResourceSafe(id = R.string.story_backup_title),
subtitleText = stringResourceSafe(id = R.string.story_backup_description),
)
},
bottomContent = {
@ -65,8 +68,8 @@ fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description),
titleText = stringResourceSafe(id = R.string.story_currencies_title),
subtitleText = stringResourceSafe(id = R.string.story_currencies_description),
)
},
bottomContent = {
@ -81,8 +84,8 @@ fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description),
titleText = stringResourceSafe(id = R.string.story_web3_title),
subtitleText = stringResourceSafe(id = R.string.story_web3_description),
)
},
bottomContent = {
@ -97,8 +100,8 @@ fun StoriesWalletForEveryone(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description),
titleText = stringResourceSafe(id = R.string.story_finish_title),
subtitleText = stringResourceSafe(id = R.string.story_finish_description),
)
},
bottomContent = {

View file

@ -16,13 +16,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.home.compose.StoriesTextAnimation
@ -66,7 +66,7 @@ fun FirstStoriesContent(isPaused: Boolean, duration: Int) {
) { modifier ->
Text(
modifier = modifier,
text = stringResource(R.string.story_meet_title),
text = stringResourceSafe(R.string.story_meet_title),
style = style,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,

View file

@ -9,12 +9,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
@ -52,7 +52,7 @@ internal fun HomeButtons(
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.home_button_scan),
text = stringResourceSafe(id = R.string.home_button_scan),
useDarkerColors = false,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
onClick = onClick,
@ -64,7 +64,7 @@ private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier:
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.home_button_order),
text = stringResourceSafe(id = R.string.home_button_order),
useDarkerColors = true,
onClick = onClick,
)

View file

@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@ -18,7 +18,7 @@ import com.tangem.wallet.R
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
StoriesButton(
modifier = modifier,
text = stringResource(id = R.string.common_search_tokens),
text = stringResourceSafe(id = R.string.common_search_tokens),
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
showProgress = false,
useDarkerColors = true,

View file

@ -1,23 +0,0 @@
package com.tangem.tap.features.home.data
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.GeoResponse
import com.tangem.tap.features.home.domain.HomeRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
/**
* Implementation of repository for Home feature
*
* @property tangemTechApi API for server requests
* @property dispatchers coroutine dispatcher provider
*/
class HomeRepositoryImpl(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : HomeRepository {
override suspend fun getUserCountryCode(): GeoResponse = withContext(dispatchers.io) {
tangemTechApi.getUserCountryCode()
}
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.home.domain
import com.tangem.datasource.api.tangemTech.models.GeoResponse
/** Repository for Home feature */
interface HomeRepository {
/** Get user's country code */
suspend fun getUserCountryCode(): GeoResponse
}

View file

@ -1,12 +0,0 @@
package com.tangem.tap.features.home.featuretoggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import javax.inject.Inject
class HomeFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isMigrateUserCountryCodeEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "MIGRATE_USER_COUNTRY_CODE_ENABLED")
}

View file

@ -53,7 +53,6 @@ private fun handleHomeAction(action: Action) {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ReadCard -> {
action.scope.launch {

View file

@ -4,6 +4,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.StateType
// todo refactor [REDACTED_TASK_KEY]
data class HomeState(
val scanInProgress: Boolean = false,
val stories: ImmutableList<Stories> = Stories.entries.toImmutableList(),

View file

@ -35,7 +35,7 @@ internal class MainScreenStateHolder(private val intents: MainIntents) {
stateFlowInternal.update { state ->
state.copy(
modalNotification = TangemBottomSheetConfig(
isShow = true,
isShown = true,
onDismissRequest = intents::onDismissBottomSheet,
content = notificationsFactory.createBalancesAreHiddenModalNotification(),
),
@ -45,7 +45,7 @@ internal class MainScreenStateHolder(private val intents: MainIntents) {
fun updateWithoutModalNotification() {
stateFlowInternal.update { state ->
state.copy(modalNotification = state.modalNotification?.copy(isShow = false))
state.copy(modalNotification = state.modalNotification?.copy(isShown = false))
}
}

View file

@ -18,7 +18,6 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
import com.tangem.tap.features.main.model.MainScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
@ -41,7 +40,6 @@ internal class MainViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val apiConfigsManager: ApiConfigsManager,
homeFeatureToggles: HomeFeatureToggles,
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel(), MainIntents {
@ -63,11 +61,9 @@ internal class MainViewModel @Inject constructor(
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
viewModelScope.launch {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
}
viewModelScope.launch {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
}
}
@ -129,7 +125,7 @@ internal class MainViewModel @Inject constructor(
it.isBalanceHidingNotificationEnabled && it.isBalanceHidden
}
.onEach {
if (state.value.modalNotification?.isShow != true && !it.isUpdateFromToast) {
if (state.value.modalNotification?.isShown != true && !it.isUpdateFromToast) {
listenToFlipsUseCase.changeUpdateEnabled(false)
stateHolder.updateWithHiddenBalancesNotification()
router.push(AppRoute.ModalNotification)

View file

@ -50,7 +50,7 @@ internal class ModalNotificationBottomSheetFragment : ComposeBottomSheetFragment
}
LaunchedEffect(notification) {
if (!notification.isShow) {
if (!notification.isShown) {
dismiss()
}
}

View file

@ -22,11 +22,9 @@ import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -50,7 +48,7 @@ object OnboardingHelper {
if (!response.twinsIsTwinned()) {
true
} else {
onboardingManager.isActivationInProgress(cardId) ?: false
onboardingManager.isActivationInProgress(cardId)
}
}
@ -62,7 +60,7 @@ object OnboardingHelper {
emptyWallets || activationInProgress || isNoBackup
}
response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) ?: false
response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId)
else -> true
}
}
@ -70,7 +68,7 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppRoute {
val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 ||
scanResponse.productType == ProductType.Ring ||
scanResponse.productType == ProductType.Wallet
scanResponse.productType == ProductType.Wallet // AppRoute.OnboardingOther is also supported
if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled &&
newOnboardingSupportTypes
) {
@ -222,7 +220,7 @@ object OnboardingHelper {
}
}
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse, globalState: GlobalState) {
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse) {
val blockchain = walletManager.wallet.blockchain
val excludedBlockchains = store.inject(DaggerGraphState::excludedBlockchains)
@ -238,17 +236,10 @@ object OnboardingHelper {
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
scope.launch {
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
val isRussia = getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
} else {
globalState.userCountryCode == RUSSIA_COUNTRY_CODE
}
if (isRussia && !onrampFeatureToggles.isFeatureEnabled) {
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))

View file

@ -176,7 +176,6 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = scanResponse,
globalState = globalState,
)
}
is OnboardingNoteAction.Done -> {

View file

@ -299,7 +299,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
OnboardingHelper.handleTopUpAction(
walletManager = walletManager,
scanResponse = onboardingManager?.scanResponse ?: return,
globalState = globalState,
)
}
TwinCardsAction.Done -> {

View file

@ -37,7 +37,6 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.datasource.utils.isNullOrEmpty
@ -301,10 +300,6 @@ class OnboardingWalletFragment :
windowSize = rememberWindowSize(activity = requireActivity()),
) {
onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize())
EventMessageEffect(
messageHandler = uiDependencies.eventMessageHandler,
snackbarHostState = uiDependencies.globalSnackbarHostState,
)
}
}
}

View file

@ -5,11 +5,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@ -17,10 +17,10 @@ import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog
@Composable
fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) {
BasicDialog(
title = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title),
message = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description),
title = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_title),
message = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_description),
confirmButton = DialogButtonUM(
title = stringResource(R.string.common_enable),
title = stringResourceSafe(R.string.common_enable),
onClick = dialog.onEnroll,
),
dismissButton = DialogButtonUM(

View file

@ -11,11 +11,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@ -63,7 +63,7 @@ private fun Header(onCloseClick: () -> Unit) {
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.secondary,
contentDescription = stringResource(id = R.string.common_cancel),
contentDescription = stringResourceSafe(id = R.string.common_cancel),
)
}
SpacerW8()
@ -85,7 +85,7 @@ private fun Title(modifier: Modifier = Modifier) {
contentDescription = null,
)
Text(
text = stringResource(id = R.string.save_user_wallet_agreement_header_biometrics),
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_header_biometrics),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
@ -102,13 +102,13 @@ private fun Description(modifier: Modifier = Modifier) {
) {
DescriptionItem(
iconPainter = painterResource(id = R.drawable.ic_face_recognition_24),
title = stringResource(id = R.string.save_user_wallet_agreement_access_title),
description = stringResource(id = R.string.save_user_wallet_agreement_access_description),
title = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_title),
description = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_description),
)
DescriptionItem(
iconPainter = painterResource(id = R.drawable.ic_lock_24),
title = stringResource(id = R.string.save_user_wallet_agreement_code_title),
description = stringResource(id = R.string.save_user_wallet_agreement_code_description_biometrics),
title = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_title),
description = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_description_biometrics),
)
}
}
@ -125,12 +125,12 @@ private fun Footer(showProgress: Boolean, onSaveWalletClick: () -> Unit) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
showProgress = showProgress,
text = stringResource(id = R.string.save_user_wallet_agreement_allow_biometrics),
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics),
onClick = onSaveWalletClick,
)
Text(
modifier = Modifier.fillMaxWidth(fraction = .7f),
text = stringResource(R.string.save_user_wallet_agreement_notice),
text = stringResourceSafe(R.string.save_user_wallet_agreement_notice),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,

View file

@ -17,7 +17,6 @@ import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -26,7 +25,6 @@ import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@Suppress("LargeClass")
@Deprecated("Will be removed soon")
object TradeCryptoMiddleware {
@ -41,23 +39,22 @@ object TradeCryptoMiddleware {
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Buy -> proceedBuyAction(action)
is TradeCryptoAction.Sell -> proceedSellAction(action)
}
}
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
private fun proceedBuyAction(action: TradeCryptoAction.Buy) {
val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled
if (isOnrampEnabled) {
proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency, action.source)
} else {
proceedWithLegacyBuyAction(state, action)
proceedWithLegacyBuyAction(action)
}
}
@ -73,7 +70,7 @@ object TradeCryptoMiddleware {
}
}
private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
private fun proceedWithLegacyBuyAction(action: TradeCryptoAction.Buy) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
?.let(NetworkAddress.Address::value)
@ -92,15 +89,10 @@ object TradeCryptoMiddleware {
)
scope.launch {
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
} else {
state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE
}
val isRussia = getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
if (action.checkUserLocation && isRussia && !onrampFeatureToggles.isFeatureEnabled) {
val dialogData = topUrl?.let {

View file

@ -0,0 +1,36 @@
package com.tangem.tap.features.welcome.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.features.welcome.model.WelcomeModel
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultWelcomeComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: WelcomeComponent.Params,
) : WelcomeComponent, AppComponentContext by context {
private val model: WelcomeModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
WelcomeScreen(
modifier = modifier,
state = state,
)
}
@AssistedFactory
interface Factory : WelcomeComponent.Factory {
override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.welcome.component
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface WelcomeComponent : ComposableContentComponent {
data class Params(
val intent: SerializableIntent?,
)
interface Factory : ComponentFactory<Params, WelcomeComponent>
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.welcome.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
internal class PreviewWelcomeComponent(
private val initialState: WelcomeScreenState = WelcomeScreenState(),
) : WelcomeComponent {
@Composable
override fun Content(modifier: Modifier) {
WelcomeScreen(
modifier = modifier,
state = initialState,
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.features.welcome.di
import com.tangem.tap.features.welcome.component.DefaultWelcomeComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindWelcomeComponentFactory(factory: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.welcome.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.welcome.model.WelcomeModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(WelcomeModel::class)
fun bindWelcomeModel(model: WelcomeModel): Model
}

View file

@ -0,0 +1,137 @@
package com.tangem.tap.features.welcome.model
import com.tangem.common.core.TangemError
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.features.welcome.redux.WelcomeState
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
// FIXME: Remove redux: [REDACTED_JIRA]
internal class WelcomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appFinisher: AppFinisher,
paramsContainer: ParamsContainer,
) : Model(), StoreSubscriber<WelcomeState> {
private val params: WelcomeComponent.Params = paramsContainer.require()
private val initialState: WelcomeScreenState = WelcomeScreenState(
onPopBack = appFinisher::finish,
onUnlockClick = this::unlockWallets,
onScanCardClick = this::scanCard,
onCloseError = this::closeError,
)
val state: MutableStateFlow<WelcomeScreenState> = MutableStateFlow(initialState)
init {
store.dispatch(WelcomeAction.SetCoroutineScope(modelScope))
subscribeToStoreChanges()
initGlobalState()
val welcomeAction = if (params.intent != null) {
WelcomeAction.ProceedWithIntent(params.intent.toIntent())
} else {
WelcomeAction.ProceedWithBiometrics()
}
store.dispatch(welcomeAction)
}
private fun unlockWallets() {
Analytics.send(SignIn.ButtonBiometricSignIn())
store.dispatch(WelcomeAction.ProceedWithBiometrics())
}
private fun scanCard() {
Analytics.send(SignIn.ButtonCardSignIn())
store.dispatch(WelcomeAction.ProceedWithCard)
}
private fun closeError() {
store.dispatch(WelcomeAction.CloseError)
}
override fun newState(state: WelcomeState) {
val warning = createWarningIfNeeded(state.error)
this.state.update { prevState ->
prevState.copy(
showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress,
showUnlockWithCardProgress = state.isUnlockWithCardInProgress,
warning = warning,
error = state.error
?.takeIf { !it.silent && warning == null }
?.let { e ->
e.messageResId?.let { TextReference.Res(it) }
?: TextReference.Str(e.customMessage)
},
)
}
}
override fun onDestroy() {
store.dispatch(WelcomeAction.ClearCoroutineScope)
store.unsubscribe(subscriber = this)
super.onDestroy()
}
private fun createWarningIfNeeded(error: TangemError?): WarningModel? {
return when (error) {
is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning(
isPermanent = error.isPermanent,
onDismiss = this::dismissWarning,
)
is UserWalletsListError.AllKeysInvalidated,
is UserWalletsListError.NoUserWalletSelected,
-> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarning,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
onDismiss = this::clearUserWallets,
)
else -> null
}
}
private fun dismissWarning() {
state.update { prevState ->
prevState.copy(
warning = null,
)
}
closeError()
}
private fun clearUserWallets() {
store.dispatch(WelcomeAction.ClearUserWallets)
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.welcomeState == new.welcomeState }
.select { it.welcomeState }
}
}
private fun initGlobalState() {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
}
}

View file

@ -1,13 +1,15 @@
package com.tangem.tap.features.welcome.ui
import androidx.compose.runtime.Immutable
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.welcome.ui.model.WarningModel
@Immutable
internal data class WelcomeScreenState(
val onPopBack: () -> Unit = {},
val showUnlockWithBiometricsProgress: Boolean = false,
val showUnlockWithCardProgress: Boolean = false,
val warning: WarningModel? = null,
val error: TextReference? = null,
val onUnlockClick: () -> Unit = {},
val onScanCardClick: () -> Unit = {},
val onCloseError: () -> Unit = {},
)

View file

@ -130,6 +130,5 @@ internal class WelcomeViewModel @Inject constructor(
private fun initGlobalState() {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(GlobalAction.FetchUserCountry)
}
}

View file

@ -4,10 +4,10 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.welcome.ui.model.WarningModel
import com.tangem.wallet.R
@ -18,8 +18,8 @@ internal fun WarningDialog(warning: WarningModel?) {
null -> Unit
is WarningModel.BiometricsLockoutWarning -> {
BasicDialog(
title = stringResource(id = R.string.biometric_lockout_warning_title),
message = stringResource(
title = stringResourceSafe(id = R.string.biometric_lockout_warning_title),
message = stringResourceSafe(
id = if (warning.isPermanent) {
R.string.biometric_lockout_permanent_warning_description
} else {
@ -28,30 +28,30 @@ internal fun WarningDialog(warning: WarningModel?) {
),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
title = stringResourceSafe(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
is WarningModel.KeyInvalidatedWarning -> {
BasicDialog(
title = stringResource(id = R.string.common_attention),
message = stringResource(id = R.string.key_invalidated_warning_description),
title = stringResourceSafe(id = R.string.common_attention),
message = stringResourceSafe(id = R.string.key_invalidated_warning_description),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
title = stringResourceSafe(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
is WarningModel.BiometricsDisabledWarning -> {
BasicDialog(
title = stringResource(id = R.string.common_warning),
message = stringResource(id = R.string.biometric_unavailable_warning),
title = stringResourceSafe(id = R.string.common_warning),
message = stringResourceSafe(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
isDismissable = false,
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
title = stringResourceSafe(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)

View file

@ -0,0 +1,101 @@
package com.tangem.tap.features.welcome.ui.components
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
import com.tangem.tap.features.welcome.ui.model.WarningModel
@Composable
internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modifier) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
val warning by rememberUpdatedState(newValue = state.warning)
BackHandler(onBack = state.onPopBack)
Box(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.systemBarsPadding(),
) {
WelcomeScreenContent(
showUnlockProgress = state.showUnlockWithBiometricsProgress,
showScanCardProgress = state.showUnlockWithCardProgress,
onUnlockClick = state.onUnlockClick,
onScanCardClick = state.onScanCardClick,
)
SnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(vertical = 16.dp)
.fillMaxWidth(),
hostState = snackbarHostState,
)
}
WarningDialog(warning)
LaunchedEffect(errorMessage, state.onCloseError) {
errorMessage?.let {
snackbarHostState.showSnackbar(it)
state.onCloseError()
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_WelcomeScreen(
@PreviewParameter(WelcomeComponentPreviewProvider::class) component: WelcomeComponent,
) {
TangemThemePreview {
component.Content(Modifier)
}
}
private class WelcomeComponentPreviewProvider : PreviewParameterProvider<WelcomeComponent> {
override val values: Sequence<WelcomeComponent>
get() = sequenceOf(
PreviewWelcomeComponent(),
PreviewWelcomeComponent(
initialState = WelcomeScreenState(
showUnlockWithBiometricsProgress = true,
showUnlockWithCardProgress = true,
),
),
PreviewWelcomeComponent(
initialState = WelcomeScreenState(
error = TextReference.Str(value = "Error"),
),
),
PreviewWelcomeComponent(
initialState = WelcomeScreenState(
warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}),
),
),
)
}
// endregion Preview

View file

@ -9,10 +9,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@ -24,8 +24,12 @@ internal fun WelcomeScreenContent(
showScanCardProgress: Boolean,
onUnlockClick: () -> Unit,
onScanCardClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerHMax()
Icon(
modifier = Modifier.size(TangemTheme.dimens.size96),
@ -38,7 +42,7 @@ internal fun WelcomeScreenContent(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(R.string.welcome_unlock_title),
text = stringResourceSafe(R.string.welcome_unlock_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
@ -48,9 +52,9 @@ internal fun WelcomeScreenContent(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing44)
.fillMaxWidth(),
text = stringResource(
text = stringResourceSafe(
id = R.string.welcome_unlock_description,
stringResource(id = R.string.common_biometric_authentication),
stringResourceSafe(id = R.string.common_biometric_authentication),
),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
@ -61,9 +65,9 @@ internal fun WelcomeScreenContent(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResource(id = R.string.common_biometrics),
stringResourceSafe(id = R.string.common_biometrics),
),
showProgress = showUnlockProgress,
onClick = onUnlockClick,
@ -73,7 +77,7 @@ internal fun WelcomeScreenContent(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(R.string.welcome_unlock_card),
text = stringResourceSafe(R.string.welcome_unlock_card),
showProgress = showScanCardProgress,
iconResId = R.drawable.ic_tangem_24,
onClick = onScanCardClick,