Updated on 2026-08-14

This commit is contained in:
Tangem 2022-11-11 23:26:10 +04:00
parent 36a3e2f7d6
commit 0cc270adc0
14 changed files with 464 additions and 1 deletions

View file

@ -22,6 +22,7 @@ import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment
@ -112,5 +113,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.QrScan -> QrScanFragment() AppScreen.QrScan -> QrScanFragment()
AppScreen.ReferralProgram -> ReferralFragment() AppScreen.ReferralProgram -> ReferralFragment()
AppScreen.Welcome -> WelcomeFragment() AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
} }
} }

View file

@ -25,6 +25,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware import com.tangem.tap.features.shop.redux.ShopMiddleware
@ -53,6 +54,7 @@ data class AppState(
val walletConnectState: WalletConnectState = WalletConnectState(), val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(), val shopState: ShopState = ShopState(),
val welcomeState: WelcomeState = WelcomeState(), val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
) : StateType { ) : StateType {
val domainState: DomainState val domainState: DomainState

View file

@ -23,5 +23,6 @@ enum class AppScreen(
WalletConnectSessions, WalletConnectSessions,
QrScan, QrScan,
ReferralProgram, ReferralProgram,
Welcome Welcome,
SaveWallet(isDialogFragment = true),
} }

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.core.TangemError
import org.rekotlin.Action
internal sealed interface SaveWalletAction : Action {
object Save : SaveWalletAction {
object Success : SaveWalletAction
data class Error(val error: TangemError) : SaveWalletAction
}
object Dismiss : SaveWalletAction
object CloseError : SaveWalletAction
object SaveWalletWasShown : SaveWalletAction
}

View file

@ -0,0 +1,5 @@
package com.tangem.tap.features.saveWallet.redux
import org.rekotlin.StateType
class SaveWalletState : StateType

View file

@ -0,0 +1,83 @@
package com.tangem.tap.features.saveWallet.ui
import android.content.DialogInterface
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.components.dialogs.EnrollBiometricsDialogContent
import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent
internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWalletScreenState>() {
override val expandedHeightFraction: Float = .95f
private val viewModel by viewModels<SaveWalletViewModel>()
override fun onDismiss(dialog: DialogInterface) {
viewModel.dismiss()
super.onDismiss(dialog)
}
@Composable
override fun provideState(): State<SaveWalletScreenState> {
return viewModel.state.collectAsState()
}
@Composable
override fun ScreenContent(
modifier: Modifier,
state: SaveWalletScreenState,
) {
val snackbarHostState = remember { SnackbarHostState() }
val errorMessage by rememberUpdatedState(newValue = state.error?.customMessage)
val enrollBiometricsDialog by rememberUpdatedState(newValue = state.enrollBiometricsDialog)
Box(modifier = modifier) {
SaveWalletScreenContent(
showProgress = state.showProgress,
onSaveWalletClick = viewModel::saveWallet,
onCloseClick = this@SaveWalletBottomSheetFragment::dismissAllowingStateLoss,
)
SnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(vertical = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
hostState = snackbarHostState,
)
}
EnrollBiometricsDialog(dialog = enrollBiometricsDialog)
LaunchedEffect(key1 = errorMessage) {
errorMessage?.let {
snackbarHostState.showSnackbar(it)
viewModel.closeError()
}
}
}
@Composable
private fun EnrollBiometricsDialog(
modifier: Modifier = Modifier,
dialog: EnrollBiometricsDialog?,
) {
if (dialog == null) return
EnrollBiometricsDialogContent(modifier, dialog)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.saveWallet.ui
import androidx.compose.runtime.Immutable
import com.tangem.common.core.TangemError
import com.tangem.core.ui.models.EnrollBiometricsDialog
@Immutable
internal data class SaveWalletScreenState(
val showProgress: Boolean = false,
val enrollBiometricsDialog: EnrollBiometricsDialog? = null,
val error: TangemError? = null,
)

View file

@ -0,0 +1,51 @@
package com.tangem.tap.features.saveWallet.ui
import androidx.lifecycle.ViewModel
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
internal class SaveWalletViewModel : ViewModel(), StoreSubscriber<SaveWalletState> {
private val stateInternal = MutableStateFlow(SaveWalletScreenState())
val state: StateFlow<SaveWalletScreenState> = stateInternal
init {
subscribeToStoreChanges()
store.dispatchOnMain(SaveWalletAction.SaveWalletWasShown)
}
fun saveWallet() {
store.dispatch(SaveWalletAction.Save)
}
fun dismiss() {
store.dispatch(SaveWalletAction.Dismiss)
}
fun closeError() {
store.dispatch(SaveWalletAction.CloseError)
}
override fun newState(state: SaveWalletState) {
stateInternal.update { prevState ->
// TODO: Update screen state
prevState
}
}
override fun onCleared() {
store.unsubscribe(this)
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.saveWalletState == new.saveWalletState }
.select { it.saveWalletState }
}
}
}

View file

@ -0,0 +1,157 @@
package com.tangem.tap.features.saveWallet.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
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.PrimaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
@Composable
internal fun SaveWalletScreenContent(
modifier: Modifier = Modifier,
showProgress: Boolean,
onSaveWalletClick: () -> Unit,
onCloseClick: () -> Unit,
) {
Column(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Hand()
IconButton(
modifier = Modifier
.align(Alignment.End)
.padding(all = TangemTheme.dimens.spacing8)
.size(TangemTheme.dimens.size32),
onClick = onCloseClick,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_close),
tint = TangemTheme.colors.icon.secondary,
contentDescription = stringResource(id = R.string.common_cancel),
)
}
Spacer(modifier = Modifier.weight(.7f))
Icon(
modifier = Modifier.size(TangemTheme.dimens.size56),
painter = painterResource(id = R.drawable.ic_fingerprint_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.text.accent.copy(alpha = .12f),
shape = TangemTheme.shapes.roundedCornersMedium,
),
) {
Text(
modifier = Modifier
.padding(
vertical = TangemTheme.dimens.spacing4,
horizontal = TangemTheme.dimens.spacing12,
),
text = stringResource(R.string.save_user_wallet_agreement_new_feature),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.accent,
textAlign = TextAlign.Center,
)
}
SpacerH24()
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.save_user_wallet_agreement_header),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH12()
Text(
modifier = Modifier.fillMaxWidth(fraction = .87f),
text = stringResource(R.string.save_user_wallet_agreement_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.weight(1f))
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
showProgress = showProgress,
text = stringResource(R.string.save_user_wallet_agreement_allow),
onClick = onSaveWalletClick,
)
SpacerH16()
Text(
modifier = Modifier.fillMaxWidth(fraction = .7f),
text = stringResource(R.string.save_user_wallet_agreement_notice),
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
SpacerH16()
}
}
// region Preview
@Composable
private fun SaveWalletScreenContentSample(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
SaveWalletScreenContent(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing32)
.fillMaxSize()
.background(
color = TangemTheme.colors.background.plain,
shape = TangemTheme.shapes.bottomSheet,
),
showProgress = false,
onSaveWalletClick = { /* no-op */ },
onCloseClick = { /* no-op */ },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SaveWalletScreenContentPreview_Light() {
TangemTheme {
SaveWalletScreenContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SaveWalletScreenContentPreview_Dark() {
TangemTheme(isDark = true) {
SaveWalletScreenContentSample()
}
}
// endregion Preview

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17.8,4.5C17.7,4.5 17.7,4.5 17.6,4.4C15.7,3.4 14,3 12,3C10,3 8.2,3.5 6.4,4.4C6.2,4.5 5.9,4.5 5.8,4.2C5.6,4 5.7,3.7 6,3.5C7.8,2.5 9.9,2 12,2C14.1,2 16,2.5 18,3.5C18.3,3.7 18.4,4 18.3,4.2C18.2,4.4 18,4.5 17.8,4.5ZM3.5,9.7C3.4,9.7 3.3,9.7 3.2,9.6C3,9.5 2.9,9.2 3.1,8.9C4.1,7.5 5.3,6.4 6.8,5.7C10,4 14,4 17.2,5.7C18.7,6.4 19.9,7.5 20.9,8.9C21.1,9.1 21,9.4 20.8,9.6C20.6,9.8 20.2,9.7 20.1,9.5C19.2,8.2 18,7.2 16.7,6.5C13.8,5.1 10.2,5.1 7.3,6.6C5.9,7.3 4.8,8.3 3.9,9.5C3.8,9.7 3.7,9.7 3.5,9.7ZM9.8,21.8C9.6,21.8 9.5,21.7 9.4,21.6C8.5,20.8 8.1,20.2 7.4,19C6.7,17.8 6.3,16.3 6.3,14.7C6.3,11.7 8.9,9.3 12,9.3C15.1,9.3 17.7,11.7 17.7,14.7C17.7,14.8 17.6,14.9 17.5,15C17.4,15.1 17.3,15.2 17.2,15.2C17,15.2 16.9,15.1 16.8,15C16.7,14.9 16.7,14.8 16.7,14.7C16.7,12.2 14.6,10.3 12,10.3C9.4,10.3 7.3,12.2 7.3,14.7C7.3,16.1 7.7,17.4 8.3,18.5C8.9,19.7 9.4,20.2 10.1,20.9C10.3,21.1 10.3,21.4 10.1,21.6C10,21.7 9.9,21.8 9.8,21.8ZM16.9,19.9C15.7,19.9 14.7,19.6 13.8,19.1C12.3,18 11.4,16.4 11.4,14.7C11.4,14.5 11.5,14.4 11.6,14.3C11.7,14.2 11.8,14.2 11.9,14.2C12.1,14.2 12.2,14.2 12.3,14.3C12.4,14.4 12.4,14.5 12.4,14.7C12.4,16.1 13.2,17.4 14.4,18.2C15.1,18.7 15.9,18.9 16.9,18.9C17.2,18.9 17.6,18.9 18,18.8C18.2,18.8 18.5,19 18.5,19.2C18.6,19.5 18.4,19.8 18.1,19.8C17.6,19.9 17.1,19.9 16.9,19.9ZM14.9,22C14.9,22 14.8,22 14.8,22C13.2,21.5 12.2,21 11.1,19.9C9.7,18.5 8.9,16.6 8.9,14.7C8.9,13 10.3,11.7 12,11.7C13.7,11.7 15.1,13 15.1,14.7C15.1,15.7 16,16.6 17.1,16.6C18.3,16.6 19.2,15.7 19.2,14.7C19.2,10.9 16,7.8 12,7.8C9.1,7.8 6.5,9.4 5.4,11.9C5,12.7 4.8,13.6 4.8,14.7C4.8,15.4 4.8,16.7 5.4,18.3C5.5,18.5 5.4,18.8 5.1,18.9C4.9,19 4.6,18.9 4.5,18.6C4,17.3 3.8,16 3.8,14.7C3.8,13.5 4,12.4 4.5,11.4C5.8,8.6 8.7,6.8 12,6.8C16.5,6.8 20.2,10.3 20.2,14.7C20.2,16.3 18.8,17.6 17.1,17.6C15.4,17.6 14.1,16.3 14.1,14.7C14.1,13.6 13.1,12.7 12,12.7C10.8,12.7 9.9,13.6 9.9,14.7C9.9,16.4 10.6,18 11.8,19.2C12.7,20.1 13.6,20.6 15,21C15.3,21.1 15.5,21.4 15.4,21.6C15.3,21.9 15.1,22 14.9,22Z" />
</vector>

View file

@ -0,0 +1,97 @@
package com.tangem.core.ui.components.dialogs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
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.window.Dialog
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.models.EnrollBiometricsDialog
import com.tangem.core.ui.res.TangemTheme
@Composable
fun EnrollBiometricsDialogContent(
modifier: Modifier = Modifier,
dialog: EnrollBiometricsDialog,
) {
Dialog(onDismissRequest = dialog.onCancel) {
Column(
modifier = modifier
.background(
shape = TangemTheme.shapes.roundedCornersLarge,
color = TangemTheme.colors.background.plain,
)
.padding(all = TangemTheme.dimens.spacing24),
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
SpacerH16()
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.save_user_wallet_agreement_enroll_biometrics_description),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
SpacerH24()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
TextButton(
text = stringResource(R.string.common_cancel),
onClick = dialog.onCancel,
)
SpacerW8()
TextButton(
text = stringResource(R.string.common_enable),
onClick = dialog.onEnroll,
)
}
}
}
}
// region Preview
@Composable
private fun EnrollBiometricDialogContentSample(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
EnrollBiometricsDialogContent(dialog = EnrollBiometricsDialog({}, {}))
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun EnrollBiometricDialogContentPreview_Light() {
TangemTheme {
EnrollBiometricDialogContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun EnrollBiometricDialogContentPreview_Dark() {
TangemTheme(isDark = true) {
EnrollBiometricDialogContentSample()
}
}
// endregion Preview

View file

@ -0,0 +1,9 @@
package com.tangem.core.ui.models
import androidx.compose.runtime.Immutable
@Immutable
data class EnrollBiometricsDialog(
val onEnroll: () -> Unit,
val onCancel: () -> Unit,
)

View file

@ -143,4 +143,13 @@
<string name="welcome_unlock_description">Используйте биометрическую аутентификацию или код доступа для входа в приложение</string> <string name="welcome_unlock_description">Используйте биометрическую аутентификацию или код доступа для входа в приложение</string>
<string name="welcome_unlock_face_id">Войти с биометрией</string> <string name="welcome_unlock_face_id">Войти с биометрией</string>
<string name="welcome_unlock_card">Сканировать карту</string> <string name="welcome_unlock_card">Сканировать карту</string>
<string name="save_user_wallet_agreement_new_feature">Новый функционал</string>
<string name="save_user_wallet_agreement_header">Сохраните свой кошелек</string>
<string name="save_user_wallet_agreement_description">Функция сохранения кошелька позволяет вам использовать свой кошелек с биометрической авторизацией, не прикладывая карту к телефону для получения доступа.</string>
<string name="save_user_wallet_agreement_allow">Разрешить привязку кошелька</string>
<string name="save_user_wallet_agreement_notice">Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта</string>
<string name="save_user_wallet_agreement_enroll_biometrics_title">Включите биометрическую авторизацию</string>
<string name="save_user_wallet_agreement_enroll_biometrics_description">Похоже, что у вас отключена биометрическая авторизация, она необходима для сохранения кошельков</string>
<string name="common_enable">Включить</string>
<string name="common_cancel">Отмена</string>
</resources> </resources>

View file

@ -145,4 +145,13 @@
<string name="welcome_unlock_description">Use biometric authentication or scan a card to access the app</string> <string name="welcome_unlock_description">Use biometric authentication or scan a card to access the app</string>
<string name="welcome_unlock_face_id">Log in with biometric</string> <string name="welcome_unlock_face_id">Log in with biometric</string>
<string name="welcome_unlock_card">Scan card</string> <string name="welcome_unlock_card">Scan card</string>
<string name="save_user_wallet_agreement_new_feature">New feature</string>
<string name="save_user_wallet_agreement_header">Save your Wallet</string>
<string name="save_user_wallet_agreement_description">Save your Wallet feature allows you to use your wallet with biometric auth without tapping your card to the phone to gain access.</string>
<string name="save_user_wallet_agreement_allow">Allow to link wallet</string>
<string name="save_user_wallet_agreement_notice">Making a transaction with your funds will still require card tapping</string>
<string name="save_user_wallet_agreement_enroll_biometrics_title">Enable biometric authorization</string>
<string name="save_user_wallet_agreement_enroll_biometrics_description">It looks like you have biometric authorization disabled, it is necessary to save wallets</string>
<string name="common_enable">Enable</string>
<string name="common_cancel">Cancel</string>
</resources> </resources>