From 0cc270adc0feca0068e9f1f53a123bec39e0ea20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Nov 2022 23:26:10 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/common/extensions/Navigation.kt | 2 + .../com/tangem/tap/common/redux/AppState.kt | 2 + .../redux/navigation/NavigationState.kt | 3 +- .../saveWallet/redux/SaveWalletAction.kt | 17 ++ .../saveWallet/redux/SaveWalletState.kt | 5 + .../ui/SaveWalletBottomSheetFragment.kt | 83 +++++++++ .../saveWallet/ui/SaveWalletScreenState.kt | 12 ++ .../saveWallet/ui/SaveWalletViewModel.kt | 51 ++++++ .../ui/components/SaveWalletScreenContent.kt | 157 ++++++++++++++++++ .../main/res/drawable/ic_fingerprint_24.xml | 9 + .../dialogs/EnrollBiometricsDialogConent.kt | 97 +++++++++++ .../core/ui/models/EnrollBiometricsDialog.kt | 9 + .../src/main/res/values-ru/strings_final.xml | 9 + core/ui/src/main/res/values/strings_final.xml | 9 + 14 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt create mode 100644 app/src/main/res/drawable/ic_fingerprint_24.xml create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/dialogs/EnrollBiometricsDialogConent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/models/EnrollBiometricsDialog.kt diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index c33e466944..dae414e733 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -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.twins.ui.TwinsCardsFragment 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.shop.ui.ShopFragment import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment @@ -112,5 +113,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.QrScan -> QrScanFragment() AppScreen.ReferralProgram -> ReferralFragment() AppScreen.Welcome -> WelcomeFragment() + AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index db49b1a222..d46d70029f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -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.OnboardingWalletState 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.states.SendState import com.tangem.tap.features.shop.redux.ShopMiddleware @@ -53,6 +54,7 @@ data class AppState( val walletConnectState: WalletConnectState = WalletConnectState(), val shopState: ShopState = ShopState(), val welcomeState: WelcomeState = WelcomeState(), + val saveWalletState: SaveWalletState = SaveWalletState(), ) : StateType { val domainState: DomainState diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index aa3765d085..ba861a55dc 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -23,5 +23,6 @@ enum class AppScreen( WalletConnectSessions, QrScan, ReferralProgram, - Welcome + Welcome, + SaveWallet(isDialogFragment = true), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt new file mode 100644 index 0000000000..3e0bbe4a7e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt new file mode 100644 index 0000000000..d43ee7c96b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt @@ -0,0 +1,5 @@ +package com.tangem.tap.features.saveWallet.redux + +import org.rekotlin.StateType + +class SaveWalletState : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt new file mode 100644 index 0000000000..6abc3c607e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt @@ -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() { + override val expandedHeightFraction: Float = .95f + + private val viewModel by viewModels() + + override fun onDismiss(dialog: DialogInterface) { + viewModel.dismiss() + super.onDismiss(dialog) + } + + @Composable + override fun provideState(): State { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt new file mode 100644 index 0000000000..9059d0d97d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt new file mode 100644 index 0000000000..24e2ef650c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -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 { + private val stateInternal = MutableStateFlow(SaveWalletScreenState()) + val state: StateFlow = 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 } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt new file mode 100644 index 0000000000..b2eeba4b72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_fingerprint_24.xml b/app/src/main/res/drawable/ic_fingerprint_24.xml new file mode 100644 index 0000000000..df5bdf4868 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dialogs/EnrollBiometricsDialogConent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dialogs/EnrollBiometricsDialogConent.kt new file mode 100644 index 0000000000..a75943cc04 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dialogs/EnrollBiometricsDialogConent.kt @@ -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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/models/EnrollBiometricsDialog.kt b/core/ui/src/main/java/com/tangem/core/ui/models/EnrollBiometricsDialog.kt new file mode 100644 index 0000000000..a48550d1c1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/models/EnrollBiometricsDialog.kt @@ -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, +) \ No newline at end of file diff --git a/core/ui/src/main/res/values-ru/strings_final.xml b/core/ui/src/main/res/values-ru/strings_final.xml index 092befb3cc..20b896eb21 100644 --- a/core/ui/src/main/res/values-ru/strings_final.xml +++ b/core/ui/src/main/res/values-ru/strings_final.xml @@ -143,4 +143,13 @@ Используйте биометрическую аутентификацию или код доступа для входа в приложение Войти с биометрией Сканировать карту + Новый функционал + Сохраните свой кошелек + Функция сохранения кошелька позволяет вам использовать свой кошелек с биометрической авторизацией, не прикладывая карту к телефону для получения доступа. + Разрешить привязку кошелька + Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта + Включите биометрическую авторизацию + Похоже, что у вас отключена биометрическая авторизация, она необходима для сохранения кошельков + Включить + Отмена diff --git a/core/ui/src/main/res/values/strings_final.xml b/core/ui/src/main/res/values/strings_final.xml index 8ded5c6e4a..a6fd518ac9 100644 --- a/core/ui/src/main/res/values/strings_final.xml +++ b/core/ui/src/main/res/values/strings_final.xml @@ -145,4 +145,13 @@ Use biometric authentication or scan a card to access the app Log in with biometric Scan card + New feature + Save your Wallet + Save your Wallet feature allows you to use your wallet with biometric auth without tapping your card to the phone to gain access. + Allow to link wallet + Making a transaction with your funds will still require card tapping + Enable biometric authorization + It looks like you have biometric authorization disabled, it is necessary to save wallets + Enable + Cancel