From 36a3e2f7d67043740bf993eed5f328aecbc70162 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Nov 2022 11:54:10 +0300 Subject: [PATCH 01/16] Updated on 2026-08-14 --- .../tap/common/extensions/Navigation.kt | 2 + .../com/tangem/tap/common/redux/AppState.kt | 5 +- .../redux/navigation/NavigationState.kt | 3 +- .../features/welcome/redux/WelcomeAction.kt | 18 +++ .../features/welcome/redux/WelcomeState.kt | 5 + .../features/welcome/ui/WelcomeFragment.kt | 74 +++++++++++ .../features/welcome/ui/WelcomeScreenState.kt | 11 ++ .../features/welcome/ui/WelcomeViewModel.kt | 49 +++++++ .../ui/components/WelcomeScreenContent.kt | 122 ++++++++++++++++++ .../main/res/drawable/img_tangem_logo_96.xml | 15 +++ .../com/tangem/core/ui/components/Spacers.kt | 7 + .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../src/main/res/values-ru/strings_final.xml | 4 + core/ui/src/main/res/values/strings_final.xml | 4 + 14 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt create mode 100644 app/src/main/res/drawable/img_tangem_logo_96.xml 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 97575ecd31..c33e466944 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 @@ -28,6 +28,7 @@ import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment import com.tangem.tap.features.tokens.ui.AddTokensFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.wallet.R import timber.log.Timber @@ -110,5 +111,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.WalletConnectSessions -> WalletConnectFragment() AppScreen.QrScan -> QrScanFragment() AppScreen.ReferralProgram -> ReferralFragment() + AppScreen.Welcome -> WelcomeFragment() } } \ 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 af79cb81e1..db49b1a222 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 @@ -33,6 +33,7 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware +import com.tangem.tap.features.welcome.redux.WelcomeState import org.rekotlin.Middleware import org.rekotlin.StateType @@ -51,6 +52,7 @@ data class AppState( val tokensState: TokensState = TokensState(), val walletConnectState: WalletConnectState = WalletConnectState(), val shopState: ShopState = ShopState(), + val welcomeState: WelcomeState = WelcomeState(), ) : StateType { val domainState: DomainState @@ -81,5 +83,4 @@ data class AppState( ) } } -} - +} \ No newline at end of file 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 6d5993cbc0..aa3765d085 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 @@ -22,5 +22,6 @@ enum class AppScreen( AddTokens, AddCustomToken, WalletConnectSessions, QrScan, - ReferralProgram + ReferralProgram, + Welcome } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt new file mode 100644 index 0000000000..f68e5122d7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.welcome.redux + +import com.tangem.common.core.TangemError +import org.rekotlin.Action + +internal sealed interface WelcomeAction : Action { + object ProceedWithBiometrics : WelcomeAction { + object Success : WelcomeAction + data class Error(val error: TangemError) : WelcomeAction + } + + object ProceedWithCard : WelcomeAction { + object Success : WelcomeAction + data class Error(val error: TangemError) : WelcomeAction + } + + object CloseError : WelcomeAction +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt new file mode 100644 index 0000000000..2d89c4d902 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt @@ -0,0 +1,5 @@ +package com.tangem.tap.features.welcome.redux + +import org.rekotlin.StateType + +class WelcomeState : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt new file mode 100644 index 0000000000..b80c8ac31b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.features.welcome.ui + +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.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.compose.ui.res.colorResource +import androidx.fragment.app.viewModels +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.fragments.ComposeFragment +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent +import com.tangem.wallet.R + +internal class WelcomeFragment : ComposeFragment() { + private val viewModel by viewModels() + + @Composable + override fun provideState(): State { + return viewModel.state.collectAsState() + } + + @Composable + override fun ScreenContent( + modifier: Modifier, + state: WelcomeScreenState, + ) { + val snackbarHostState = remember { SnackbarHostState() } + val errorMessage by rememberUpdatedState(newValue = state.error?.customMessage) + + val backgroundColor = colorResource(id = R.color.background_primary) + SystemBarsEffect { + setSystemBarsColor(color = backgroundColor) + } + + Box( + modifier = modifier + .systemBarsPadding(), + ) { + WelcomeScreenContent( + showUnlockProgress = state.showUnlockWithBiometricsProgress, + showScanCardProgress = state.showUnlockWithCardProgress, + onUnlockClick = viewModel::unlockWallets, + onScanCardClick = viewModel::scanCard, + ) + + SnackbarHost( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + hostState = snackbarHostState, + ) + } + + LaunchedEffect(key1 = errorMessage) { + errorMessage?.let { + snackbarHostState.showSnackbar(it) + viewModel.closeError() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt new file mode 100644 index 0000000000..7a741a391f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.welcome.ui + +import androidx.compose.runtime.Immutable +import com.tangem.common.core.TangemError + +@Immutable +internal data class WelcomeScreenState( + val showUnlockWithBiometricsProgress: Boolean = false, + val showUnlockWithCardProgress: Boolean = false, + val error: TangemError? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt new file mode 100644 index 0000000000..ab9e8e6673 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -0,0 +1,49 @@ +package com.tangem.tap.features.welcome.ui + +import androidx.lifecycle.ViewModel +import com.tangem.tap.features.welcome.redux.WelcomeAction +import com.tangem.tap.features.welcome.redux.WelcomeState +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 WelcomeViewModel : ViewModel(), StoreSubscriber { + private val stateInternal = MutableStateFlow(WelcomeScreenState()) + val state: StateFlow = stateInternal + + init { + subscribeToStoreChanges() + } + + fun unlockWallets() { + store.dispatch(WelcomeAction.ProceedWithBiometrics) + } + + fun scanCard() { + store.dispatch(WelcomeAction.ProceedWithCard) + } + + fun closeError() { + store.dispatch(WelcomeAction.CloseError) + } + + override fun newState(state: WelcomeState) { + 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.welcomeState == new.welcomeState } + .select { it.welcomeState } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt new file mode 100644 index 0000000000..f9543d5c97 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt @@ -0,0 +1,122 @@ +package com.tangem.tap.features.welcome.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.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.PrimaryButtonIconRight +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.res.TangemTheme +import com.tangem.wallet.R + +@Composable +internal fun WelcomeScreenContent( + modifier: Modifier = Modifier, + showUnlockProgress: Boolean, + showScanCardProgress: Boolean, + onUnlockClick: () -> Unit, + onScanCardClick: () -> Unit, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerHMax() + Icon( + modifier = Modifier.size(TangemTheme.dimens.size96), + painter = painterResource(id = R.drawable.img_tangem_logo_96), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + SpacerH32() + Text( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResource(R.string.welcome_unlock_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH12() + Text( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing44) + .fillMaxWidth(), + text = stringResource(R.string.welcome_unlock_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerHMax() + SecondaryButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResource(R.string.welcome_unlock_face_id), + showProgress = showUnlockProgress, + onClick = onUnlockClick, + ) + SpacerH12() + PrimaryButtonIconRight( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResource(R.string.welcome_unlock_card), + showProgress = showScanCardProgress, + icon = painterResource(id = R.drawable.ic_tangem), + onClick = onScanCardClick, + ) + SpacerH16() + } +} + +// region Preview +@Composable +private fun WelcomeScreenContentSample( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + WelcomeScreenContent( + showUnlockProgress = false, + showScanCardProgress = false, + onUnlockClick = { /* no-op */ }, + onScanCardClick = { /* no-op */ }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun WelcomeScreenContentPreview_Light() { + TangemTheme { + WelcomeScreenContentSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun WelcomeScreenContentPreview_Dark() { + TangemTheme(isDark = true) { + WelcomeScreenContentSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/res/drawable/img_tangem_logo_96.xml b/app/src/main/res/drawable/img_tangem_logo_96.xml new file mode 100644 index 0000000000..93fe8d9c0f --- /dev/null +++ b/app/src/main/res/drawable/img_tangem_logo_96.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt index 8293073012..a6425652bb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components import androidx.annotation.DimenRes +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size @@ -58,6 +59,12 @@ fun SpacerH24(modifier: Modifier = Modifier) { fun SpacerH32(modifier: Modifier = Modifier) { SpacerH(32.dp, modifier) } + +// TODO: Refactor with context receivers +@Composable +fun ColumnScope.SpacerHMax(modifier: Modifier = Modifier) { + Spacer(modifier = modifier.weight(1f)) +} // endregion Horizontal // region Vertical diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 4732b620be..a05eb630a4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -42,6 +42,7 @@ data class TangemDimens internal constructor( val size56: Dp = 56.dp, val size84: Dp = 84.dp, val size86: Dp = 86.dp, + val size96: Dp = 96.dp, val size102: Dp = 102.dp, val size164: Dp = 164.dp, val size200: Dp = 200.dp, 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 5b98f5cc07..092befb3cc 100644 --- a/core/ui/src/main/res/values-ru/strings_final.xml +++ b/core/ui/src/main/res/values-ru/strings_final.xml @@ -139,4 +139,8 @@ В этом случае вам будет необходимо начать процесс заново. Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. Внутренняя ошибка: не удается найти менеджер кошельков + C возвращением! + Используйте биометрическую аутентификацию или код доступа для входа в приложение + Войти с биометрией + Сканировать карту diff --git a/core/ui/src/main/res/values/strings_final.xml b/core/ui/src/main/res/values/strings_final.xml index 6f46bdd1be..8ded5c6e4a 100644 --- a/core/ui/src/main/res/values/strings_final.xml +++ b/core/ui/src/main/res/values/strings_final.xml @@ -141,4 +141,8 @@ In this case, you will need to start from the beginning. You have used a card from another wallet. Tap the card associated with this wallet Internal error: wallet manager not found + Welcome back! + Use biometric authentication or scan a card to access the app + Log in with biometric + Scan card From 0cc270adc0feca0068e9f1f53a123bec39e0ea20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Nov 2022 23:26:10 +0400 Subject: [PATCH 02/16] 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 From 47412b1d63c4bff9184de93490504413e73a73a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 12 Nov 2022 00:36:45 +0400 Subject: [PATCH 03/16] Updated on 2026-08-14 --- .../tap/common/extensions/Navigation.kt | 10 +- .../com/tangem/tap/common/redux/AppState.kt | 2 + .../redux/navigation/NavigationState.kt | 1 + .../redux/WalletSelectorAction.kt | 31 +++ .../redux/WalletSelectorState.kt | 5 + .../ui/WalletSelectorBottomSheetFragment.kt | 80 ++++++ .../ui/WalletSelectorScreenState.kt | 20 ++ .../ui/WalletSelectorViewModel.kt | 143 ++++++++++ .../walletSelector/ui/components/MockData.kt | 30 +++ .../components/RenameWalletDialogContent.kt | 111 ++++++++ .../WallectSelectorScreenContent.kt | 251 ++++++++++++++++++ .../ui/components/WalletItem.kt | 204 ++++++++++++++ .../ui/components/WalletList.kt | 66 +++++ .../ui/model/RenameWalletDialog.kt | 10 + .../walletSelector/ui/model/UserWalletItem.kt | 46 ++++ app/src/main/res/drawable/ic_check_24.xml | 9 + .../main/res/drawable/ic_check_circle_18.xml | 9 + app/src/main/res/drawable/ic_locked_24.xml | 9 + .../res/drawable/ic_pencil_outline_24.xml | 9 + .../com/tangem/core/ui/components/Spacers.kt | 17 ++ .../com/tangem/core/ui/res/TangemDimens.kt | 3 + .../res/values-de/strings_plurals_final.xml | 5 + .../res/values-fr/strings_plurals_final.xml | 6 + .../res/values-it/strings_plurals_final.xml | 5 + .../src/main/res/values-ru/strings_final.xml | 8 + .../res/values-ru/strings_plurals_final.xml | 7 + core/ui/src/main/res/values/strings_final.xml | 8 + .../main/res/values/strings_plurals_final.xml | 7 + 28 files changed, 1110 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorScreenState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/RenameWalletDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt create mode 100644 app/src/main/res/drawable/ic_check_24.xml create mode 100644 app/src/main/res/drawable/ic_check_circle_18.xml create mode 100644 app/src/main/res/drawable/ic_locked_24.xml create mode 100644 app/src/main/res/drawable/ic_pencil_outline_24.xml 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 dae414e733..3abbbac6d1 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 @@ -29,6 +29,7 @@ import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment import com.tangem.tap.features.tokens.ui.AddTokensFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.wallet.R import timber.log.Timber @@ -52,10 +53,14 @@ fun FragmentActivity.openFragment( } if (screen.isDialogFragment) { (fragment as DialogFragment).show(transaction, screen.name) - if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null) + if (addToBackstack) { + transaction.addToBackStack(null) + } } else { transaction.replace(R.id.fragment_container, fragment, screen.name) - if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null) + if (addToBackstack && (screen != AppScreen.Home && screen != AppScreen.Welcome)) { + transaction.addToBackStack(null) + } transaction.commitAllowingStateLoss() } } @@ -114,5 +119,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.ReferralProgram -> ReferralFragment() AppScreen.Welcome -> WelcomeFragment() AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() + AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment() } } \ 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 d46d70029f..a158f2c872 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 @@ -35,6 +35,7 @@ import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState +import com.tangem.tap.features.walletSelector.redux.WalletSelectorState import org.rekotlin.Middleware import org.rekotlin.StateType @@ -55,6 +56,7 @@ data class AppState( val shopState: ShopState = ShopState(), val welcomeState: WelcomeState = WelcomeState(), val saveWalletState: SaveWalletState = SaveWalletState(), + val walletSelectorState: WalletSelectorState = WalletSelectorState(), ) : 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 ba861a55dc..e3d82a292a 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 @@ -25,4 +25,5 @@ enum class AppScreen( ReferralProgram, Welcome, SaveWallet(isDialogFragment = true), + WalletSelector(isDialogFragment = true), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt new file mode 100644 index 0000000000..51a7253075 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.features.walletSelector.redux + +import com.tangem.common.core.TangemError +import org.rekotlin.Action + +internal sealed interface WalletSelectorAction : Action { + object UnlockWithBiometry : WalletSelectorAction { + object Success : WalletSelectorAction + data class Error(val error: TangemError) : WalletSelectorAction + } + + data class SelectWallet( + val walletId: String, + ) : WalletSelectorAction + + data class RenameWallet( + val walletId: String, + val newName: String, + ) : WalletSelectorAction + + data class RemoveWallets( + val walletIdsToRemove: List, + ) : WalletSelectorAction + + object AddWallet : WalletSelectorAction { + object Success : WalletSelectorAction + data class Error(val error: TangemError) : WalletSelectorAction + } + + object CloseError : WalletSelectorAction +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt new file mode 100644 index 0000000000..c84839277a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt @@ -0,0 +1,5 @@ +package com.tangem.tap.features.walletSelector.redux + +import org.rekotlin.StateType + +class WalletSelectorState : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt new file mode 100644 index 0000000000..47c6ca9bf2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -0,0 +1,80 @@ +package com.tangem.tap.features.walletSelector.ui + +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.fragments.ComposeBottomSheetFragment +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent +import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent +import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog + +internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment() { + private val viewModel by viewModels() + + @Composable + override fun provideState(): State { + return viewModel.state.collectAsState() + } + + @Composable + override fun ScreenContent( + modifier: Modifier, + state: WalletSelectorScreenState, + ) { + val snackbarHostState = remember { SnackbarHostState() } + val errorMessage by rememberUpdatedState(newValue = state.error?.customMessage) + val renameWalletDialog by rememberUpdatedState(newValue = state.renameWalletDialog) + + Box(modifier = modifier) { + WalletSelectorScreenContent( + state = state, + onWalletClick = viewModel::walletClicked, + onWalletLongClick = viewModel::walletLongClicked, + onUnlockClick = viewModel::unlock, + onAddCardClick = viewModel::addWallet, + onClearSelectedClick = viewModel::cancelWalletsEditing, + onEditSelectedWalletClick = viewModel::renameWallet, + onDeleteSelectedWalletsClick = viewModel::deleteWallets, + ) + + SnackbarHost( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(vertical = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + hostState = snackbarHostState, + ) + } + + RenameWalletDialog(dialog = renameWalletDialog) + + LaunchedEffect(key1 = errorMessage) { + errorMessage?.let { + snackbarHostState.showSnackbar(it) + viewModel.closeError() + } + } + } + + @Composable + private fun RenameWalletDialog( + modifier: Modifier = Modifier, + dialog: RenameWalletDialog?, + ) { + if (dialog == null) return + RenameWalletDialogContent(modifier, dialog) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorScreenState.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorScreenState.kt new file mode 100644 index 0000000000..bc3016f8f2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorScreenState.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.features.walletSelector.ui + +import androidx.compose.runtime.Immutable +import com.tangem.common.core.TangemError +import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog +import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem + +@Immutable +internal data class WalletSelectorScreenState( + val wallets: List = listOf(), + val selectedWalletId: String? = null, + val fiatCurrency: FiatCurrency = FiatCurrency.Default, + val isLocked: Boolean = true, + val editingWalletsIds: List = listOf(), + val renameWalletDialog: RenameWalletDialog? = null, + val showAddCardProgress: Boolean = false, + val showUnlockProgress: Boolean = false, + val error: TangemError? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt new file mode 100644 index 0000000000..cb6d75b0b0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt @@ -0,0 +1,143 @@ +package com.tangem.tap.features.walletSelector.ui + +import androidx.lifecycle.ViewModel +import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction +import com.tangem.tap.features.walletSelector.redux.WalletSelectorState +import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog +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 WalletSelectorViewModel : ViewModel(), StoreSubscriber { + private val stateInternal = MutableStateFlow(WalletSelectorScreenState()) + val state: StateFlow = stateInternal + + init { + subscribeToStoreChanges() + bootstrapWalletListChanges() + bootstrapWalletsStoresChanges() + } + + fun unlock() { + store.dispatch(WalletSelectorAction.UnlockWithBiometry) + } + + fun addWallet() { + store.dispatch(WalletSelectorAction.AddWallet) + } + + fun walletClicked(walletId: String) = with(state.value) { + when { + isLocked -> Unit + editingWalletsIds.isNotEmpty() && !editingWalletsIds.contains(walletId) -> { + editWallet(walletId) + } + editingWalletsIds.isNotEmpty() && editingWalletsIds.contains(walletId) -> { + cancelWalletEditing(walletId) + } + selectedWalletId != walletId -> { + store.dispatch(WalletSelectorAction.SelectWallet(walletId)) + } + } + } + + fun walletLongClicked(walletId: String) = with(state.value) { + if (!isLocked && editingWalletsIds.isEmpty()) { + editWallet(walletId) + } + } + + fun cancelWalletsEditing() { + stateInternal.update { prevState -> + prevState.copy( + editingWalletsIds = emptyList(), + ) + } + } + + fun renameWallet() = with(state.value) { + if (editingWalletsIds.isNotEmpty() && renameWalletDialog == null) { + val editedWalletId = editingWalletsIds.first() + val editedWallet = wallets.find { it.id == editedWalletId } + + if (editedWallet != null) { + val dialog = RenameWalletDialog( + currentName = editedWallet.name, + onApply = { newName -> + store.dispatch(WalletSelectorAction.RenameWallet(editedWalletId, newName)) + stateInternal.update { prevState -> + prevState.copy( + renameWalletDialog = null, + editingWalletsIds = emptyList(), + ) + } + }, + onCancel = { + stateInternal.update { prevState -> + prevState.copy( + renameWalletDialog = null, + ) + } + }, + ) + + stateInternal.update { prevState -> + prevState.copy( + renameWalletDialog = dialog, + ) + } + } + } + } + + fun deleteWallets() = with(state.value) { + if (editingWalletsIds.isNotEmpty()) { + store.dispatch(WalletSelectorAction.RemoveWallets(walletIdsToRemove = editingWalletsIds)) + } + } + + fun closeError() { + store.dispatch(WalletSelectorAction.CloseError) + } + + override fun newState(state: WalletSelectorState) { + // TODO: Update UI state + } + + override fun onCleared() { + store.unsubscribe(this) + } + + private fun editWallet(walletId: String) { + stateInternal.update { prevState -> + prevState.copy( + editingWalletsIds = prevState.editingWalletsIds + walletId, + ) + } + } + + private fun cancelWalletEditing(walletId: String) { + stateInternal.update { prevState -> + prevState.copy( + editingWalletsIds = prevState.editingWalletsIds - walletId, + ) + } + } + + private fun subscribeToStoreChanges() { + store.subscribe(this) { appState -> + appState.skip { old, new -> old.walletSelectorState == new.walletSelectorState } + .select { it.walletSelectorState } + } + } + + private fun bootstrapWalletListChanges() { + // TODO + } + + private fun bootstrapWalletsStoresChanges() { + // TODO + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt new file mode 100644 index 0000000000..7fe1623321 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/MockData.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.walletSelector.ui.components + +import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState +import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem + +internal object MockData { + private val userWallet = UserWalletItem( + id = "wallet_1", + balance = UserWalletItem.Balance( + amount = "6781.05 $", + isLoading = false, + ), + name = "Wallet", + imageUrl = "https://app.tangem.com/cards/card_default.png", + type = UserWalletItem.Type.MultiCurrency(tokensCount = 12, cardsInWallet = 3), + ) + + val userWalletList = listOf( + userWallet, + userWallet.copy(id = "wallet_2"), + userWallet.copy(id = "wallet_3", type = UserWalletItem.Type.MultiCurrency(tokensCount = 2, cardsInWallet = 1)), + userWallet.copy(id = "wallet_4", type = UserWalletItem.Type.SingleCurrency(tokenName = "Ethereum")), + ) + + val state = WalletSelectorScreenState( + wallets = userWalletList, + selectedWalletId = userWalletList[0].id, + isLocked = false, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt new file mode 100644 index 0000000000..f6aa55d7a0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/RenameWalletDialogContent.kt @@ -0,0 +1,111 @@ +package com.tangem.tap.features.walletSelector.ui.components + +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.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import com.tangem.core.ui.components.OutlineTextField +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.res.TangemTheme +import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog +import com.tangem.wallet.R + +@Composable +internal fun RenameWalletDialogContent( + modifier: Modifier = Modifier, + dialog: RenameWalletDialog, +) { + Dialog(onDismissRequest = dialog.onCancel) { + Column( + modifier = modifier + .background( + shape = TangemTheme.shapes.roundedCornersLarge, + color = TangemTheme.colors.background.plain, + ) + .padding(all = TangemTheme.dimens.spacing24), + ) { + var value by remember { + mutableStateOf(TextFieldValue(text = dialog.currentName)) + } + + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.user_wallet_list_rename_popup_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + SpacerH16() + OutlineTextField( + value = value, + label = stringResource(R.string.user_wallet_list_rename_popup_placeholder), + onValueChange = { newValue -> + value = newValue + }, + ) + SpacerH24() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton( + text = stringResource(id = R.string.common_cancel), + onClick = dialog.onCancel, + ) + SpacerW8() + TextButton( + text = stringResource(id = R.string.common_save), + enabled = value.text.isNotEmpty() && value.text != dialog.currentName, + onClick = { + dialog.onApply(value.text) + }, + ) + } + } + } +} + +// region Preview +@Composable +private fun RenameWalletDialogContentSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + RenameWalletDialogContent(dialog = RenameWalletDialog("", {}, {})) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun RenameWalletDialogContentPreview_Light() { + TangemTheme { + RenameWalletDialogContentSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun RenameWalletDialogContentPreview_Dark() { + TangemTheme(isDark = true) { + RenameWalletDialogContentSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt new file mode 100644 index 0000000000..1fe2d1e173 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt @@ -0,0 +1,251 @@ +package com.tangem.tap.features.walletSelector.ui.components + +import androidx.compose.foundation.background +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.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.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +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.SecondaryButtonIconRight +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.SpacerW16 +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.walletSelector.ui.WalletSelectorScreenState +import com.tangem.wallet.R + +@Composable +internal fun WalletSelectorScreenContent( + modifier: Modifier = Modifier, + state: WalletSelectorScreenState, + onWalletClick: (walletId: String) -> Unit, + onWalletLongClick: (walletId: String) -> Unit, + onUnlockClick: () -> Unit, + onAddCardClick: () -> Unit, + onClearSelectedClick: () -> Unit, + onEditSelectedWalletClick: () -> Unit, + onDeleteSelectedWalletsClick: () -> Unit, +) { + val editingWalletsSize by rememberUpdatedState(newValue = state.editingWalletsIds.size) + + Column(modifier = modifier) { + Hand() + SpacerH12() + if (editingWalletsSize > 0) { + EditWalletsBar( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + editingWalletsSize = editingWalletsSize, + onClearSelectedClick = onClearSelectedClick, + onEditSelectedWalletClick = onEditSelectedWalletClick, + onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick, + ) + } else { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.user_wallet_list_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + } + SpacerH12() + WalletList( + wallets = state.wallets, + selectedWalletId = state.selectedWalletId, + checkedWalletIds = state.editingWalletsIds, + isLocked = state.isLocked, + onWalletClick = onWalletClick, + onWalletLongClick = onWalletLongClick, + ) + SpacerH24() + if (state.isLocked) { + PrimaryButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResource(R.string.user_wallet_list_unlock_all_face_id), + showProgress = state.showUnlockProgress, + onClick = onUnlockClick, + ) + SpacerH12() + } + SecondaryButtonIconRight( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResource(R.string.user_wallet_list_add_button), + showProgress = state.showAddCardProgress, + icon = painterResource(id = R.drawable.ic_tangem), + onClick = onAddCardClick, + ) + SpacerH16() + } +} + +@Composable +private fun EditWalletsBar( + modifier: Modifier = Modifier, + editingWalletsSize: Int, + onClearSelectedClick: () -> Unit, + onEditSelectedWalletClick: () -> Unit, + onDeleteSelectedWalletsClick: () -> Unit, +) { + val showEditAction = remember(editingWalletsSize) { editingWalletsSize in 1 until 2 } + Row( + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing8) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onClearSelectedClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_close), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Unselect wallets", + ) + } + SpacerW16() + Text( + text = stringResource(id = R.string.user_wallet_list_editing_count, editingWalletsSize), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + if (showEditAction) { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onEditSelectedWalletClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_pencil_outline_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Edit wallet name", + ) + } + SpacerW8() + } + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onDeleteSelectedWalletsClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_trash), + tint = TangemTheme.colors.icon.warning, + contentDescription = "Delete selected wallets", + ) + } + } +} + +// region Preview +@Composable +private fun WalletSelectorScreenContentSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.primary), + ) { + WalletSelectorScreenContent( + modifier = Modifier + .background( + color = TangemTheme.colors.background.plain, + shape = TangemTheme.shapes.bottomSheet, + ), + state = MockData.state.copy(isLocked = true), + onWalletClick = { /* no-op */ }, + onWalletLongClick = { /* no-op */ }, + onUnlockClick = { /* no-op */ }, + onAddCardClick = { /* no-op */ }, + onEditSelectedWalletClick = { /* no-op */ }, + onClearSelectedClick = { /* no-op */ }, + onDeleteSelectedWalletsClick = { /* no-op */ }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun WalletSelectorScreenContentPreview_Light() { + TangemTheme { + WalletSelectorScreenContentSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun WalletSelectorScreenContentPreview_Dark() { + TangemTheme(isDark = true) { + WalletSelectorScreenContentSample() + } +} + +@Composable +private fun WalletSelectorScreenContent_EditWallets_Sample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + WalletSelectorScreenContent( + modifier = Modifier + .background( + color = TangemTheme.colors.background.plain, + shape = TangemTheme.shapes.bottomSheet, + ), + state = MockData.state + .copy(editingWalletsIds = listOf(MockData.userWalletList[2].id)), + onWalletClick = { /* no-op */ }, + onWalletLongClick = { /* no-op */ }, + onUnlockClick = { /* no-op */ }, + onAddCardClick = { /* no-op */ }, + onEditSelectedWalletClick = { /* no-op */ }, + onClearSelectedClick = { /* no-op */ }, + onDeleteSelectedWalletsClick = { /* no-op */ }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun WalletSelectorScreenContent_EditWallets_Preview_Light() { + TangemTheme { + WalletSelectorScreenContent_EditWallets_Sample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun WalletSelectorScreenContent_EditWallets_Preview_Dark() { + TangemTheme(isDark = true) { + WalletSelectorScreenContent_EditWallets_Sample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt new file mode 100644 index 0000000000..6837d47a7f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -0,0 +1,204 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + +package com.tangem.tap.features.walletSelector.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.components.SpacerW6 +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem +import com.tangem.wallet.R + +@Composable +internal fun WalletItem( + modifier: Modifier = Modifier, + wallet: UserWalletItem, + isSelected: Boolean, + isChecked: Boolean, + isLocked: Boolean, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .width(TangemTheme.dimens.size62) + .height(TangemTheme.dimens.size42), + ) { + val cardImageModifier = Modifier + .align(Alignment.Center) + .width(TangemTheme.dimens.size56) + .height(TangemTheme.dimens.size32) + .clip(TangemTheme.shapes.roundedCornersSmall) + SubcomposeAsyncImage( + modifier = cardImageModifier, + model = ImageRequest.Builder(LocalContext.current) + .data(wallet.imageUrl) + .crossfade(true) + .build(), + loading = { CardImagePlaceholder(modifier = cardImageModifier) }, + error = { CardImagePlaceholder(modifier = cardImageModifier) }, + contentDescription = null, + ) + when { + isChecked -> { + Box( + modifier = cardImageModifier + .background( + color = TangemTheme.colors.icon.accent.copy(alpha = .5f), + ), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.primary2, + contentDescription = null, + ) + } + } + isSelected -> { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size18) + .background( + color = Color.White, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .padding(all = (0.5).dp), + painter = painterResource(id = R.drawable.ic_check_circle_18), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } + else -> Unit + } + } + SpacerW8() + Column( + modifier = Modifier.weight(weight = .6f), + verticalArrangement = Arrangement.SpaceAround, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = wallet.name, + color = if (isSelected) TangemTheme.colors.text.accent else TangemTheme.colors.text.primary1, + style = TangemTypography.subtitle1, + ) + SpacerH2() + Text( + modifier = Modifier.fillMaxWidth(), + text = when (val type = wallet.type) { + is UserWalletItem.Type.MultiCurrency -> pluralStringResource( + id = R.plurals.card_label_card_count, + count = type.cardsInWallet, + type.cardsInWallet, + ) + + is UserWalletItem.Type.SingleCurrency -> type.tokenName + }, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + ) + } + SpacerW6() + if (isLocked) { + Box( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .background( + color = TangemTheme.colors.button.secondary, + shape = TangemTheme.shapes.roundedCornersMedium, + ), + ) { + Icon( + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing8, + horizontal = TangemTheme.dimens.spacing12, + ) + .size(TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_locked_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } + } else { + Column( + modifier = Modifier.weight(weight = .4f), + verticalArrangement = Arrangement.SpaceAround, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = wallet.balance.amount, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ) + when (val type = wallet.type) { + is UserWalletItem.Type.MultiCurrency -> { + SpacerH2() + Text( + modifier = Modifier.fillMaxWidth(), + text = pluralStringResource( + id = R.plurals.tokens_count, + count = type.tokensCount, + type.tokensCount, + ), + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + + is UserWalletItem.Type.SingleCurrency -> Unit + } + } + } + } +} + +@Composable +private fun CardImagePlaceholder( + modifier: Modifier = Modifier, +) { + Image( + modifier = modifier, + painter = painterResource(id = R.drawable.card_placeholder_black), + contentDescription = null, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt new file mode 100644 index 0000000000..ab1f4f9c40 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt @@ -0,0 +1,66 @@ +@file:OptIn(ExperimentalFoundationApi::class) + +package com.tangem.tap.features.walletSelector.ui.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.cardsettings.resolveReference +import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem + +@Composable +internal fun WalletList( + modifier: Modifier = Modifier, + wallets: List, + selectedWalletId: String?, + checkedWalletIds: List, + isLocked: Boolean, + onWalletClick: (walletId: String) -> Unit, + onWalletLongClick: (walletId: String) -> Unit, +) { + val multiCurrencyWallets = remember(wallets) { + wallets.filter { it.type is UserWalletItem.Type.MultiCurrency } + } + val singleCurrencyWallets = remember(wallets) { + wallets.filter { it.type is UserWalletItem.Type.SingleCurrency } + } + LazyColumn(modifier = modifier) { + val walletSection: (List) -> Unit = { wallets -> + itemsIndexed( + items = wallets, + key = { _, item -> item.id }, + ) { index, wallet -> + if (index == 0) { + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), + text = wallet.type.headerText.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } + WalletItem( + modifier = Modifier + .combinedClickable( + onClick = { onWalletClick(wallet.id) }, + onLongClick = { onWalletLongClick(wallet.id) }, + ) + .padding(all = TangemTheme.dimens.spacing16), + wallet = wallet, + isSelected = wallet.id == selectedWalletId, + isChecked = wallet.id in checkedWalletIds, + isLocked = isLocked && wallet.id != selectedWalletId, + ) + } + } + + walletSection(multiCurrencyWallets) + walletSection(singleCurrencyWallets) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/RenameWalletDialog.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/RenameWalletDialog.kt new file mode 100644 index 0000000000..e3969634ce --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/RenameWalletDialog.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.walletSelector.ui.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class RenameWalletDialog( + val currentName: String, + val onApply: (newName: String) -> Unit, + val onCancel: () -> Unit, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt new file mode 100644 index 0000000000..41f0ace5cd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt @@ -0,0 +1,46 @@ +package com.tangem.tap.features.walletSelector.ui.model + +import androidx.compose.runtime.Immutable +import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.wallet.R + +@Immutable +internal data class UserWalletItem( + val id: String, + val name: String, + val imageUrl: String, + val balance: Balance, + val type: Type, +) { + @Immutable + data class Balance( + val amount: String, + val isLoading: Boolean, + ) { + companion object { + val Loading = Balance( + amount = "0.00", + isLoading = true, + ) + } + } + + sealed interface Type { + val headerText: TextReference + + @Immutable + class MultiCurrency( + val tokensCount: Int, + val cardsInWallet: Int, + ) : Type { + override val headerText: TextReference + get() = TextReference.Res(R.string.user_wallet_list_multi_header) + } + + @Immutable + class SingleCurrency(val tokenName: String) : Type { + override val headerText: TextReference + get() = TextReference.Res(R.string.user_wallet_list_single_header) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_check_24.xml b/app/src/main/res/drawable/ic_check_24.xml new file mode 100644 index 0000000000..4291149aff --- /dev/null +++ b/app/src/main/res/drawable/ic_check_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle_18.xml b/app/src/main/res/drawable/ic_check_circle_18.xml new file mode 100644 index 0000000000..2c3e494f73 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle_18.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_locked_24.xml b/app/src/main/res/drawable/ic_locked_24.xml new file mode 100644 index 0000000000..2afb4d8a14 --- /dev/null +++ b/app/src/main/res/drawable/ic_locked_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pencil_outline_24.xml b/app/src/main/res/drawable/ic_pencil_outline_24.xml new file mode 100644 index 0000000000..039a5d5463 --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt index a6425652bb..7b99b3f27e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -4,6 +4,7 @@ package com.tangem.core.ui.components import androidx.annotation.DimenRes import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size @@ -30,6 +31,11 @@ fun SpacerH(height: Dp, modifier: Modifier = Modifier) { Spacer(modifier = modifier.height(height)) } +@Composable +fun SpacerH2(modifier: Modifier = Modifier) { + SpacerH(2.dp, modifier) +} + @Composable fun SpacerH4(modifier: Modifier = Modifier) { SpacerH(4.dp, modifier) @@ -78,6 +84,11 @@ fun SpacerW4(modifier: Modifier = Modifier) { SpacerW(4.dp, modifier) } +@Composable +fun SpacerW6(modifier: Modifier = Modifier) { + SpacerW(6.dp, modifier) +} + @Composable fun SpacerW8(modifier: Modifier = Modifier) { SpacerW(8.dp, modifier) @@ -97,6 +108,12 @@ fun SpacerW24(modifier: Modifier = Modifier) { fun SpacerW32(modifier: Modifier = Modifier) { SpacerW(32.dp, modifier) } + +// TODO: Refactor with context receivers +@Composable +fun RowScope.SpacerWMax(modifier: Modifier = Modifier) { + Spacer(modifier = modifier.weight(1f)) +} // endregion Vertical // region Size diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index a05eb630a4..731400086c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -33,13 +33,16 @@ data class TangemDimens internal constructor( val size8: Dp = 8.dp, val size11: Dp = 11.dp, val size16: Dp = 16.dp, + val size18: Dp = 18.dp, val size20: Dp = 20.dp, val size24: Dp = 24.dp, val size28: Dp = 28.dp, val size32: Dp = 32.dp, val size40: Dp = 40.dp, + val size42: Dp = 42.dp, val size48: Dp = 48.dp, val size56: Dp = 56.dp, + val size62: Dp = 62.dp, val size84: Dp = 84.dp, val size86: Dp = 86.dp, val size96: Dp = 96.dp, diff --git a/core/ui/src/main/res/values-de/strings_plurals_final.xml b/core/ui/src/main/res/values-de/strings_plurals_final.xml index fa02d656a3..02204c79ec 100644 --- a/core/ui/src/main/res/values-de/strings_plurals_final.xml +++ b/core/ui/src/main/res/values-de/strings_plurals_final.xml @@ -3,4 +3,9 @@ %d card %d cards + + + %d token + %d tokens + diff --git a/core/ui/src/main/res/values-fr/strings_plurals_final.xml b/core/ui/src/main/res/values-fr/strings_plurals_final.xml index f7ce3f895b..e79bd3f0cd 100644 --- a/core/ui/src/main/res/values-fr/strings_plurals_final.xml +++ b/core/ui/src/main/res/values-fr/strings_plurals_final.xml @@ -4,4 +4,10 @@ %d cards %d cards + + + %d token + %d tokens + %d tokens + diff --git a/core/ui/src/main/res/values-it/strings_plurals_final.xml b/core/ui/src/main/res/values-it/strings_plurals_final.xml index fa02d656a3..02204c79ec 100644 --- a/core/ui/src/main/res/values-it/strings_plurals_final.xml +++ b/core/ui/src/main/res/values-it/strings_plurals_final.xml @@ -3,4 +3,9 @@ %d card %d cards + + + %d token + %d tokens + 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 20b896eb21..c8db954763 100644 --- a/core/ui/src/main/res/values-ru/strings_final.xml +++ b/core/ui/src/main/res/values-ru/strings_final.xml @@ -152,4 +152,12 @@ Похоже, что у вас отключена биометрическая авторизация, она необходима для сохранения кошельков Включить Отмена + Мультивалютные + Одновалютные + Переименование кошелька + Имя кошелька + Разблокировать все с биометрией + Добавить новый кошелек + %d выбрано + Мои кошельки diff --git a/core/ui/src/main/res/values-ru/strings_plurals_final.xml b/core/ui/src/main/res/values-ru/strings_plurals_final.xml index 53720a4bb6..35486659e4 100644 --- a/core/ui/src/main/res/values-ru/strings_plurals_final.xml +++ b/core/ui/src/main/res/values-ru/strings_plurals_final.xml @@ -5,4 +5,11 @@ %d карт %d карт + + + %d токен + %d токена + %d токенов + %d токенов + diff --git a/core/ui/src/main/res/values/strings_final.xml b/core/ui/src/main/res/values/strings_final.xml index a6fd518ac9..6f508e1d28 100644 --- a/core/ui/src/main/res/values/strings_final.xml +++ b/core/ui/src/main/res/values/strings_final.xml @@ -154,4 +154,12 @@ It looks like you have biometric authorization disabled, it is necessary to save wallets Enable Cancel + Multi-currency + Single-currency + Rename Wallet + Wallet name + Unlock all with biometrics + Add new wallet + %d selected + My Wallets diff --git a/core/ui/src/main/res/values/strings_plurals_final.xml b/core/ui/src/main/res/values/strings_plurals_final.xml index 530ada144e..9fa94fe676 100644 --- a/core/ui/src/main/res/values/strings_plurals_final.xml +++ b/core/ui/src/main/res/values/strings_plurals_final.xml @@ -5,4 +5,11 @@ %d cards %d cards + + + %d tokens + %d token + %d tokens + %d tokens + From 4375e7687f292267199558759820d35efdaa9221 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 12 Nov 2022 03:41:55 +0400 Subject: [PATCH 04/16] Updated on 2026-08-14 --- .../java/com/tangem/domain/common/CardDTO.kt | 300 ++++++++++++++++++ .../domain/common/util/CardExtensions.kt | 12 + .../tangem/domain/common/util/UserWalletId.kt | 37 +++ 3 files changed, 349 insertions(+) create mode 100644 domain/src/main/java/com/tangem/domain/common/CardDTO.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/util/CardExtensions.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt diff --git a/domain/src/main/java/com/tangem/domain/common/CardDTO.kt b/domain/src/main/java/com/tangem/domain/common/CardDTO.kt new file mode 100644 index 0000000000..58df56c687 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/CardDTO.kt @@ -0,0 +1,300 @@ +package com.tangem.domain.common + +import com.squareup.moshi.JsonClass +import com.tangem.common.card.Card +import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.EncryptionMode +import com.tangem.common.hdWallet.DerivationPath +import com.tangem.common.hdWallet.ExtendedPublicKey +import com.tangem.operations.attestation.Attestation +import java.util.* +import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion + +/** + * [Card] copy + * */ +@JsonClass(generateAdapter = true) +data class CardDTO( + val cardId: String, + val batchId: String, + val cardPublicKey: ByteArray, + val firmwareVersion: FirmwareVersion, + val manufacturer: Manufacturer, + val issuer: Issuer, + val settings: Settings, + val linkedTerminalStatus: LinkedTerminalStatus, + val isAccessCodeSet: Boolean, + val isPasscodeSet: Boolean?, + val supportedCurves: List, + val wallets: List, + val attestation: Attestation, + val backupStatus: BackupStatus?, +) { + constructor(card: Card) : this( + cardId = card.cardId, + batchId = card.batchId, + cardPublicKey = card.cardPublicKey, + firmwareVersion = FirmwareVersion(card.firmwareVersion), + manufacturer = Manufacturer(card.manufacturer), + issuer = Issuer(card.issuer), + settings = Settings(card.settings), + linkedTerminalStatus = LinkedTerminalStatus.fromSdkStatus(card.linkedTerminalStatus), + isAccessCodeSet = card.isAccessCodeSet, + isPasscodeSet = card.isPasscodeSet, + supportedCurves = card.supportedCurves, + wallets = card.wallets.map { Wallet(it) }, + attestation = card.attestation, + backupStatus = BackupStatus.fromSdkStatus(card.backupStatus), + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CardDTO) return false + + if (cardId != other.cardId) return false + if (batchId != other.batchId) return false + if (!cardPublicKey.contentEquals(other.cardPublicKey)) return false + if (firmwareVersion != other.firmwareVersion) return false + if (manufacturer != other.manufacturer) return false + if (issuer != other.issuer) return false + if (settings != other.settings) return false + if (linkedTerminalStatus != other.linkedTerminalStatus) return false + if (isAccessCodeSet != other.isAccessCodeSet) return false + if (isPasscodeSet != other.isPasscodeSet) return false + if (supportedCurves != other.supportedCurves) return false + if (wallets != other.wallets) return false + if (attestation != other.attestation) return false + + return true + } + + override fun hashCode(): Int { + var result = cardId.hashCode() + result = 31 * result + batchId.hashCode() + result = 31 * result + cardPublicKey.contentHashCode() + result = 31 * result + firmwareVersion.hashCode() + result = 31 * result + manufacturer.hashCode() + result = 31 * result + issuer.hashCode() + result = 31 * result + settings.hashCode() + result = 31 * result + linkedTerminalStatus.hashCode() + result = 31 * result + isAccessCodeSet.hashCode() + result = 31 * result + (isPasscodeSet?.hashCode() ?: 0) + result = 31 * result + supportedCurves.hashCode() + result = 31 * result + wallets.hashCode() + result = 31 * result + attestation.hashCode() + return result + } + + @JsonClass(generateAdapter = true) + data class Settings( + val securityDelay: Int, + val maxWalletsCount: Int, + val isSettingAccessCodeAllowed: Boolean, + val isSettingPasscodeAllowed: Boolean, + val isResettingUserCodesAllowed: Boolean, + val isLinkedTerminalEnabled: Boolean, + val isBackupAllowed: Boolean, + val supportedEncryptionModes: List, + val isFilesAllowed: Boolean, + val isHDWalletAllowed: Boolean, + ) { + constructor(settings: Card.Settings) : this( + securityDelay = settings.securityDelay, + maxWalletsCount = settings.maxWalletsCount, + isSettingAccessCodeAllowed = settings.isSettingAccessCodeAllowed, + isSettingPasscodeAllowed = settings.isSettingPasscodeAllowed, + isResettingUserCodesAllowed = settings.isResettingUserCodesAllowed, + isLinkedTerminalEnabled = settings.isLinkedTerminalEnabled, + isBackupAllowed = settings.isBackupAllowed, + supportedEncryptionModes = settings.supportedEncryptionModes, + isFilesAllowed = settings.isFilesAllowed, + isHDWalletAllowed = settings.isHDWalletAllowed, + ) + } + + @JsonClass(generateAdapter = true) + data class FirmwareVersion( + val major: Int, + val minor: Int, + val patch: Int, + val type: SdkFirmwareVersion.FirmwareType, + ) : Comparable { + constructor(firmwareVersion: SdkFirmwareVersion) : this( + major = firmwareVersion.major, + minor = firmwareVersion.minor, + patch = firmwareVersion.patch, + type = firmwareVersion.type, + ) + + val stringValue: String + get() = StringBuilder() + .append("$major.$minor") + .append(if (patch != 0) ".$patch" else "") + .append(type.rawValue ?: "") + .toString() + + override fun compareTo(other: SdkFirmwareVersion): Int = when { + major != other.major -> major.compareTo(other.major) + minor != other.minor -> minor.compareTo(other.minor) + else -> patch.compareTo(other.patch) + } + } + + @JsonClass(generateAdapter = true) + data class Manufacturer( + val name: String, + val manufactureDate: Date, + val signature: ByteArray?, + ) { + constructor(manufacturer: Card.Manufacturer) : this( + name = manufacturer.name, + manufactureDate = manufacturer.manufactureDate, + signature = manufacturer.signature, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Manufacturer) return false + + if (name != other.name) return false + if (manufactureDate != other.manufactureDate) return false + if (signature != null) { + if (other.signature == null) return false + if (!signature.contentEquals(other.signature)) return false + } else if (other.signature != null) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + manufactureDate.hashCode() + result = 31 * result + (signature?.contentHashCode() ?: 0) + return result + } + } + + @JsonClass(generateAdapter = true) + data class Issuer( + val name: String, + val publicKey: ByteArray, + ) { + constructor(issuer: Card.Issuer) : this( + name = issuer.name, + publicKey = issuer.publicKey, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Issuer) return false + + if (name != other.name) return false + if (!publicKey.contentEquals(other.publicKey)) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + publicKey.contentHashCode() + return result + } + } + + @JsonClass(generateAdapter = true) + data class Wallet( + val publicKey: ByteArray, + val chainCode: ByteArray?, + val curve: EllipticCurve, + val settings: CardWallet.Settings, + val totalSignedHashes: Int?, + val remainingSignatures: Int?, + val index: Int, + val hasBackup: Boolean, + val derivedKeys: Map, + val extendedPublicKey: ExtendedPublicKey?, + ) { + constructor(wallet: CardWallet) : this( + publicKey = wallet.publicKey, + chainCode = wallet.chainCode, + curve = wallet.curve, + settings = wallet.settings, + totalSignedHashes = wallet.totalSignedHashes, + remainingSignatures = wallet.remainingSignatures, + index = wallet.index, + hasBackup = wallet.hasBackup, + derivedKeys = wallet.derivedKeys, + extendedPublicKey = wallet.extendedPublicKey, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Wallet) return false + + if (!publicKey.contentEquals(other.publicKey)) return false + if (chainCode != null) { + if (other.chainCode == null) return false + if (!chainCode.contentEquals(other.chainCode)) return false + } else if (other.chainCode != null) return false + if (curve != other.curve) return false + if (settings != other.settings) return false + if (totalSignedHashes != other.totalSignedHashes) return false + if (remainingSignatures != other.remainingSignatures) return false + if (index != other.index) return false + if (hasBackup != other.hasBackup) return false + + return true + } + + override fun hashCode(): Int { + var result = publicKey.contentHashCode() + result = 31 * result + (chainCode?.contentHashCode() ?: 0) + result = 31 * result + curve.hashCode() + result = 31 * result + settings.hashCode() + result = 31 * result + (totalSignedHashes ?: 0) + result = 31 * result + (remainingSignatures ?: 0) + result = 31 * result + index + result = 31 * result + hasBackup.hashCode() + return result + } + } + + enum class LinkedTerminalStatus { + Current, + Other, + None; + + companion object { + internal fun fromSdkStatus(sdkStatus: Card.LinkedTerminalStatus): LinkedTerminalStatus { + return when (sdkStatus) { + Card.LinkedTerminalStatus.Current -> Current + Card.LinkedTerminalStatus.Other -> Other + Card.LinkedTerminalStatus.None -> None + } + } + } + } + + sealed class BackupStatus { + data class CardLinked(val cardCount: Int) : BackupStatus() + + data class Active(val cardCount: Int) : BackupStatus() + + object NoBackup : BackupStatus() + + val isActive: Boolean + get() = this is Active || this is CardLinked + + companion object { + internal fun fromSdkStatus(sdkStatus: Card.BackupStatus?): BackupStatus? { + return when (sdkStatus) { + is Card.BackupStatus.NoBackup -> NoBackup + is Card.BackupStatus.CardLinked -> CardLinked(sdkStatus.cardCount) + is Card.BackupStatus.Active -> Active(sdkStatus.cardCount) + null -> null + } + } + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/util/CardExtensions.kt b/domain/src/main/java/com/tangem/domain/common/util/CardExtensions.kt new file mode 100644 index 0000000000..0e516d7b8e --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/util/CardExtensions.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.common.util + +import com.tangem.domain.common.CardDTO + +val CardDTO.userWalletId: UserWalletId + get() = UserWalletId(findWalletPublicKey(wallets)) + +private fun findWalletPublicKey(wallets: List): ByteArray { + return wallets.firstOrNull() + ?.publicKey + ?: error("Wallet not found") +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt b/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt new file mode 100644 index 0000000000..4c8bedcd22 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.common.util + +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString +import com.tangem.domain.common.extensions.calculateHmacSha256 + +class UserWalletId( + val stringValue: String, +) { + val value = stringValue.hexToBytes() + + constructor(walletPublicKey: ByteArray) : this( + stringValue = calculateUserWalletId(walletPublicKey).toHexString(), + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UserWalletId) return false + + if (stringValue != other.stringValue) return false + + return true + } + + override fun hashCode(): Int { + return stringValue.hashCode() + } +} + +private fun calculateUserWalletId(publicKey: ByteArray): ByteArray { + val message = MESSAGE_FOR_WALLET_ID.toByteArray() + val keyHash = publicKey.calculateSha256() + return message.calculateHmacSha256(keyHash) +} + +private const val MESSAGE_FOR_WALLET_ID = "UserWalletID" \ No newline at end of file From 0d993b5141832225c2ae0bc4537193f5dd4612af Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 12 Nov 2022 03:46:02 +0400 Subject: [PATCH 05/16] Updated on 2026-08-14 --- .../tap/common/analytics/TangemSdkError.kt | 4 +- .../converters/BasicEventConverter.kt | 6 +- .../common/feedback/AdditionalFeedbackInfo.kt | 4 +- .../tap/common/redux/global/GlobalReducer.kt | 20 ++-- .../com/tangem/tap/domain/TangemSdkManager.kt | 26 +++-- .../com/tangem/tap/domain/TangemSigner.kt | 4 +- .../java/com/tangem/tap/domain/TapErrors.kt | 5 +- .../com/tangem/tap/domain/TapWalletManager.kt | 4 +- .../com/tangem/tap/domain/extensions/Card.kt | 65 ++++-------- .../domain/extensions/WalletManagerFactory.kt | 7 +- .../tasks/product/CreateProductWalletTask.kt | 100 +++++++++++++----- .../tasks/product/ProductCommandProcessor.kt | 4 +- .../domain/tasks/product/ScanProductTask.kt | 83 ++++++++++----- .../tangem/tap/domain/termsOfUse/CardTou.kt | 6 +- .../tap/domain/tokens/CurrenciesRepository.kt | 7 +- .../tap/domain/tokens/UserTokensRepository.kt | 33 +++--- .../domain/tokens/UserTokensStorageService.kt | 4 +- .../domain/tokens/models/BlockchainNetwork.kt | 12 ++- .../domain/twins/CreateFirstTwinWalletTask.kt | 1 - .../twins/CreateSecondTwinWalletTask.kt | 4 +- .../tap/domain/twins/TwinCardsManager.kt | 5 +- .../twins/WriteProtectedIssuerDataTask.kt | 3 +- .../tangem/tap/features/demo/Extentions.kt | 4 +- .../features/details/redux/DetailsAction.kt | 4 +- .../details/redux/DetailsMiddleware.kt | 34 +++--- .../features/details/redux/DetailsReducer.kt | 23 ++-- .../features/details/redux/DetailsState.kt | 10 +- .../walletconnect/WalletConnectMiddleware.kt | 4 +- .../disclaimer/redux/DisclaimerState.kt | 4 +- .../tap/features/home/redux/HomeAction.kt | 2 +- .../tap/features/home/redux/HomeMiddleware.kt | 12 +-- .../features/onboarding/OnboardingHelper.kt | 3 +- .../note/redux/OnboardingNoteMiddleware.kt | 3 +- .../redux/OnboardingOtherCardsMiddleware.kt | 35 ++++-- .../twins/redux/TwinCardsMiddleware.kt | 9 +- .../redux/OnboardingWalletMiddleware.kt | 28 +++-- .../saltPay/SaltPayActivationManager.kt | 6 +- .../send/redux/middlewares/SendMiddleware.kt | 4 +- .../tap/features/wallet/models/Currency.kt | 20 ++-- .../tap/features/wallet/redux/WalletAction.kt | 15 +-- .../redux/middlewares/WarningsMiddleware.kt | 10 +- .../wallet/ui/wallet/MultiWalletView.kt | 6 +- .../exchangeServices/CardExchangeRules.kt | 4 +- .../CurrencyExchangeManager.kt | 5 +- .../com/tangem/tap/proxy/AppStateHolder.kt | 4 +- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 8 +- buildSrc/src/main/java/Versions.kt | 9 +- .../com/tangem/domain/common/ScanResponse.kt | 18 +--- .../tangem/domain/common/TapWorkarounds.kt | 61 ++++++----- .../com/tangem/domain/common/TwinsHelper.kt | 7 +- .../domain/common/extensions/CardSdk.kt | 11 +- .../tangem/domain/common/util/UserWalletId.kt | 4 +- .../redux/AddCustomTokenState.kt | 12 +-- 53 files changed, 430 insertions(+), 356 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt index 17e31d88c4..491ef8f683 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt @@ -74,7 +74,7 @@ object TangemSdk { is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig() is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError(error.customMessage) is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage) - is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.throwable) + is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.cause) is TangemSdkError.TooMuchBackupCards -> TangemSdkError.TooMuchBackupCards() is TangemSdkError.BackupCardRequired -> TangemSdkError.BackupCardRequired() is TangemSdkError.CertificateSignatureRequired -> TangemSdkError.CertificateSignatureRequired() @@ -107,6 +107,8 @@ object TangemSdk { is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware() is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode() is TangemSdkError.BackupFailedIncompatibleBatch -> TangemSdkError.BackupFailedIncompatibleBatch() + is TangemSdkError.BiometricsUnavailable -> error + is TangemSdkError.BiometricsAuthenticationFailed -> error } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt index 3950f458bc..301ae0cb07 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt @@ -3,10 +3,10 @@ package com.tangem.tap.common.analytics.converters import com.tangem.common.Converter import com.tangem.common.extensions.isZero import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.util.userWalletId import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.filters.BasicTopUpFilter -import com.tangem.tap.domain.extensions.getUserWalletId import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletData @@ -29,7 +29,7 @@ class BasicSignInEventConverter( currency = cardCurrency, batch = scanResponse.card.batchId, ).apply { - filterData = scanResponse.card.getUserWalletId() + filterData = scanResponse.card.userWalletId.stringValue } } } @@ -43,7 +43,7 @@ class BasicTopUpEventConverter( val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null val data = BasicTopUpFilter.Data( - walletId = scanResponse.card.getUserWalletId(), + walletId = scanResponse.card.userWalletId.stringValue, cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsData), ) diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 632f9f965b..48527059ad 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -8,7 +8,7 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address -import com.tangem.common.card.CardWallet +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.domain.extensions.getUserWalletId @@ -92,7 +92,7 @@ class AdditionalFeedbackInfo { ) } - private fun formatSignedHashes(wallets: List): String { + private fun formatSignedHashes(wallets: List): String { return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index f8563c233a..f4aa80ad03 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction +import com.tangem.tap.common.extensions.replaceBy import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.preferencesStorage @@ -49,12 +50,19 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager) is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanResponse?.card ?: return globalState - val wallet = card.wallet(action.walletPublicKey) ?: return globalState - val newCardInstance = card.updateWallet( - wallet.copy( - totalSignedHashes = action.walletSignedHashes, - remainingSignatures = action.remainingSignatures, - ), + val wallet = card.wallets + .firstOrNull { it.publicKey.contentEquals(action.walletPublicKey) } + ?: return globalState + + val newCardInstance = card.copy( + wallets = card.wallets.toMutableList().also { walletsMutable -> + walletsMutable.replaceBy( + item = wallet.copy( + totalSignedHashes = action.walletSignedHashes, + remainingSignatures = action.remainingSignatures, + ), + ) { it.index == wallet.index } + }, ) globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance)) } diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 4fe7252ac7..66b16d1cad 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -1,7 +1,5 @@ package com.tangem.tap.domain -import CreateProductWalletTask -import CreateProductWalletTaskResponse import android.content.Context import androidx.annotation.StringRes import com.tangem.Message @@ -10,7 +8,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CardFilter import com.tangem.common.CompletionResult import com.tangem.common.SuccessResponse -import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.CardSessionRunnable @@ -18,6 +15,8 @@ import com.tangem.common.core.Config import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.common.map +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.operations.CommandResponse import com.tangem.operations.ScanTask @@ -29,6 +28,8 @@ import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask +import com.tangem.tap.domain.tasks.product.CreateProductWalletTask +import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tokens.UserTokensRepository @@ -73,12 +74,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } } - suspend fun createWallet(cardId: String?): CompletionResult { + suspend fun createWallet(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( CreateWalletAndRescanTask(), cardId, initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)), ) + .map { CardDTO(it) } } suspend fun derivePublicKeys( @@ -88,12 +90,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } - suspend fun resetToFactorySettings(card: Card): CompletionResult { + suspend fun resetToFactorySettings(cardId: String): CompletionResult { return runTaskAsyncReturnOnMain( - ResetToFactorySettingsTask(), - card.cardId, + runnable = ResetToFactorySettingsTask(), + cardId = cardId, initialMessage = Message(context.getString(R.string.card_settings_reset_card_to_factory)), ) + .map { CardDTO(it) } } suspend fun setPasscode(cardId: String?): CompletionResult { @@ -128,11 +131,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co ) } - suspend fun scanCard(): CompletionResult { + suspend fun scanCard(): CompletionResult { return runTaskAsyncReturnOnMain( - ScanTask(), + runnable = ScanTask(), initialMessage = Message(context.getString(R.string.initial_message_tap_header)), ) + .map { CardDTO(it) } } suspend fun runTaskAsync( @@ -150,7 +154,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } private suspend fun runTaskAsyncReturnOnMain( - runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, + runnable: CardSessionRunnable, + cardId: String? = null, + initialMessage: Message? = null, ): CompletionResult { val result = runTaskAsync(runnable, cardId, initialMessage) return withContext(Dispatchers.Main) { result } diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt index af5cdb6a5c..1979b1df8a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt @@ -5,13 +5,13 @@ import com.tangem.TangemSdk import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.Wallet import com.tangem.common.CompletionResult -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.tap.domain.tasks.SignHashesTask import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class TangemSigner( - private val card: Card, + private val card: CardDTO, private val tangemSdk: TangemSdk, private val initialMessage: Message, private val accessCode: String? = null, diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 5a0cf85668..0bc9a2c283 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -64,8 +64,7 @@ sealed class TapError( ) : TapError(-1), MultiMessageError } -sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError { - final override val code: Int = 50100 +sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) { override var customMessage: String = code.toString() object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card) @@ -88,4 +87,4 @@ fun TangemTechError.toTapError(): TapError { } } -class NoDataError(message: String) : TapError.CustomError(customMessage = message) +class NoDataError(message: String) : TapError.CustomError(customMessage = message) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index b15f2a7431..bb07af060e 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -6,8 +6,8 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.common.card.Card import com.tangem.common.services.Result +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTestCard @@ -96,7 +96,7 @@ class TapWalletManager { store.dispatch( WalletAction.MultiWallet.ShowWalletBackupWarning( show = data.card.settings.isBackupAllowed - && data.card.backupStatus == Card.BackupStatus.NoBackup, + && data.card.backupStatus == CardDTO.BackupStatus.NoBackup, ), ) loadData(data) diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index ab2ba5610b..fd46ad2fde 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -1,58 +1,45 @@ package com.tangem.tap.domain.extensions -import com.tangem.common.card.Card -import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion -import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemNote import com.tangem.domain.common.TwinCardNumber -import com.tangem.domain.common.extensions.calculateHmacSha256 import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.common.isTangemTwin import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.features.wallet.redux.Artwork -val Card.remainingSignatures: Int? - get() = this.getSingleWallet()?.remainingSignatures +val CardDTO.remainingSignatures: Int? + get() = this.wallets.firstOrNull()?.remainingSignatures -val Card.isWalletDataSupported: Boolean +val CardDTO.isWalletDataSupported: Boolean get() = this.firmwareVersion.major >= 4 -val Card.isMultiwalletAllowed: Boolean +val CardDTO.isMultiwalletAllowed: Boolean get() { - return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay - && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || - getSingleWallet()?.curve == EllipticCurve.Secp256k1) + return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay && + (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || + wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1) } -val Card.isHdWalletAllowedByApp: Boolean +val CardDTO.isHdWalletAllowedByApp: Boolean get() = settings.isHDWalletAllowed && !isSaltPay -fun Card.getSingleWallet(): CardWallet? { - return wallets.firstOrNull() +fun CardDTO.hasSignedHashes(): Boolean { + return wallets.any { (it.totalSignedHashes ?: 0) > 0 } } -fun Card.hasWallets(): Boolean = wallets.isNotEmpty() - -fun Card.hasNoWallets(): Boolean = wallets.isEmpty() - -fun Card.hasSingleWallet(): Boolean = wallets.size == 1 - -fun Card.hasSignedHashes(): Boolean { - return wallets.any { it.totalSignedHashes ?: 0 > 0 } +fun CardDTO.signedHashesCount(): Int { + return wallets.sumOf { it.totalSignedHashes ?: 0 } } -fun Card.signedHashesCount(): Int { - return wallets.map { it.totalSignedHashes ?: 0 }.sum() -} - -suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String { +suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String { fun ifAnyError(): String { return when { cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL @@ -67,42 +54,28 @@ suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result { val artworkId = cardInfoResult.data.artwork?.id - if (artworkId == null || artworkId.isEmpty()) { + if (artworkId.isNullOrEmpty()) { ifAnyError() } else { OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) } } + is Result.Failure -> ifAnyError() } } -fun Card.getArtworkUrl(artworkId: String?): String? { +fun CardDTO.getArtworkUrl(artworkId: String?): String? { return when { artworkId != null -> { OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) } + cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL else -> null } -} - -fun Card.getUserWalletId(): String { - val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return "" - return UserWalletId(walletPublicKey).stringValue -} - -class UserWalletId(val walletPublicKey: ByteArray) { - val stringValue: String = calculateUserId(walletPublicKey) - - private fun calculateUserId(walletPublicKey: ByteArray): String { - val message = "UserWalletID".toByteArray() - val keyHash = walletPublicKey.calculateSha256() - return message.calculateHmacSha256(keyHash).toHexString() - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index ec95e13dc6..874e562a2a 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -7,13 +7,12 @@ import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.common.card.Card -import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.SaltPayWorkaround import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard @@ -82,7 +81,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( ) } -private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? { +private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? { return derivationPath?.let { DerivationParams.Custom( DerivationPath(it), @@ -132,7 +131,7 @@ fun WalletManagerFactory.makePrimaryWalletManager( ) } -private fun selectWallet(wallets: List): CardWallet? { +private fun selectWallet(wallets: List): CardDTO.Wallet? { return when (wallets.size) { 0 -> null 1 -> wallets[0] diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 982ed86857..91006e8dab 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -1,3 +1,5 @@ +package com.tangem.tap.domain.tasks.product + import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card @@ -9,6 +11,8 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.common.map +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.KeyWalletPublicKey import com.tangem.domain.common.ProductType import com.tangem.domain.common.TapWorkarounds.derivationStyle @@ -19,19 +23,37 @@ import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask -import com.tangem.tap.domain.tasks.product.CreateWalletsTask -import com.tangem.tap.domain.tasks.product.ProductCommandProcessor -import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets import com.tangem.tap.features.demo.DemoHelper - +import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse data class CreateProductWalletTaskResponse( - val card: Card, + val card: CardDTO, val derivedKeys: Map = mapOf(), - val primaryCard: PrimaryCard? = null -) : CommandResponse + val primaryCard: PrimaryCard? = null, +) : CommandResponse { + constructor( + card: Card, + derivedKeys: Map = mapOf(), + primaryCard: PrimaryCard? = null, + ) : this( + card = CardDTO(card), + derivedKeys = derivedKeys, + primaryCard = primaryCard, + ) +} + +private data class CreateWalletResponse( + val cardId: String, + val wallet: CardDTO.Wallet, +) { + constructor( + sdkResponse: SdkCreateWalletResponse, + ) : this( + cardId = sdkResponse.cardId, + wallet = CardDTO.Wallet(sdkResponse.wallet), + ) +} class CreateProductWalletTask( private val type: ProductType, @@ -39,29 +61,32 @@ class CreateProductWalletTask( override fun run( session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit, ) { val card = session.environment.card.guard { callback(CompletionResult.Failure(TangemSdkError.CardError())) return } + val cardDto = CardDTO(card) val commandProcessor = when (type) { ProductType.Note -> CreateWalletTangemNote() ProductType.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") else -> CreateWalletTangemWallet() } - commandProcessor.proceed(card, session) { + commandProcessor.proceed(cardDto, session) { when (it) { is CompletionResult.Success -> { val result = when (commandProcessor) { is CreateWalletTangemWallet -> { it.data as CreateProductWalletTaskResponse } + else -> CreateProductWalletTaskResponse(card = session.environment.card!!) } callback(CompletionResult.Success(result)) } + is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error)) } } @@ -70,7 +95,7 @@ class CreateProductWalletTask( private class CreateWalletTangemNote : ProductCommandProcessor { override fun proceed( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { @@ -94,52 +119,65 @@ private class CreateWalletTangemNote : ProductCommandProcessor + callback(result.map { CreateWalletResponse(it) }) + } } } } private class CreateWalletTangemWallet : ProductCommandProcessor { - private lateinit var card: Card private var primaryCard: PrimaryCard? = null override fun proceed( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { - this.card = card val curves = card.getCurvesForNonCreatedWallets() if (curves.isEmpty()) { - val createWalletResponses = card.wallets.map { CreateWalletResponse(card.cardId, it) } - proceedWithCreatedWallets(createWalletResponses, session, callback) + val createWalletResponses = card.wallets.map { wallet -> + CreateWalletResponse(card.cardId, wallet) + } + proceedWithCreatedWallets(card, createWalletResponses, session, callback) return } CreateWalletsTask(curves).run(session) { result -> when (result) { is CompletionResult.Success -> { - proceedWithCreatedWallets(result.data.createWalletResponses, session, callback) + proceedWithCreatedWallets( + card = card, + createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) }, + session = session, + callback = callback, + ) + } + + is CompletionResult.Failure -> { + callback(CompletionResult.Failure(result.error)) } - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } } } private fun proceedWithCreatedWallets( + card: CardDTO, createWalletResponses: List, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { when { card.settings.isBackupAllowed -> { - linkPrimaryCard(createWalletResponses, session, callback) + linkPrimaryCard(card, createWalletResponses, session, callback) } + card.settings.isHDWalletAllowed -> { - deriveKeys(createWalletResponses, session, callback) + deriveKeys(card, createWalletResponses, session, callback) } + else -> { callback( CompletionResult.Success( @@ -151,6 +189,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor, session: CardSession, callback: (result: CompletionResult) -> Unit, @@ -161,8 +200,9 @@ private class CreateWalletTangemWallet : ProductCommandProcessor { - deriveKeys(createWalletResponse, session, callback) + deriveKeys(card, createWalletResponse, session, callback) } + else -> { callback( CompletionResult.Success( @@ -174,6 +214,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor { callback(CompletionResult.Failure(result.error)) } @@ -182,13 +223,14 @@ private class CreateWalletTangemWallet : ProductCommandProcessor, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { val map = mutableMapOf>() createWalletResponse.forEach { response -> - val blockchainsForCurve = getBlockchains(response.cardId).filter { + val blockchainsForCurve = getBlockchains(response.cardId, card).filter { it.getSupportedCurves().contains(response.wallet.curve) } val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath(card.derivationStyle) } @@ -210,17 +252,21 @@ private class CreateWalletTangemWallet : ProductCommandProcessor callback(CompletionResult.Failure(result.error)) } } } - private fun getBlockchains(cardId: String): List { + private fun getBlockchains( + cardId: String, + card: CardDTO, + ): List { return when { DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt index c0810ff887..8b9d8d9d2f 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt @@ -1,15 +1,15 @@ package com.tangem.tap.domain.tasks.product import com.tangem.common.CompletionResult -import com.tangem.common.card.Card import com.tangem.common.core.CardSession +import com.tangem.domain.common.CardDTO /** [REDACTED_AUTHOR] */ interface ProductCommandProcessor { fun proceed( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 459191b260..575f5fc05f 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -13,6 +13,7 @@ import com.tangem.common.extensions.guard import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ProductType import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isExcluded @@ -31,7 +32,6 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.domain.TapSdkError import com.tangem.tap.domain.extensions.getPrimaryCurve -import com.tangem.tap.domain.extensions.getSingleWallet import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.models.BlockchainNetwork @@ -53,19 +53,20 @@ class ScanProductTask( callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead())) return } + val cardDto = CardDTO(card) - val error = getErrorIfExcludedCard(card) + val error = getErrorIfExcludedCard(cardDto) if (error != null) { callback(CompletionResult.Failure(error)) return } val commandProcessor = when { - card.isTangemNote -> ScanNoteProcessor() - card.isTangemTwins -> ScanTwinProcessor() + cardDto.isTangemNote -> ScanNoteProcessor() + cardDto.isTangemTwins -> ScanTwinProcessor() else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive) } - commandProcessor.proceed(card, session) { processorResult -> + commandProcessor.proceed(cardDto, session) { processorResult -> when (processorResult) { is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult -> when (scanTaskResult) { @@ -73,7 +74,7 @@ class ScanProductTask( // it need because processorResult.data.card doesn't contains attestation result // and CardWallet.derivedKeys val processorScanResponseWithNewCard = processorResult.data.copy( - card = scanTaskResult.data, + card = CardDTO(scanTaskResult.data), ) callback(CompletionResult.Success(processorScanResponseWithNewCard)) } @@ -85,7 +86,7 @@ class ScanProductTask( } } - private fun getErrorIfExcludedCard(card: Card): TangemError? { + private fun getErrorIfExcludedCard(card: CardDTO): TangemError? { if (card.isExcluded) return TapSdkError.CardForDifferentApp if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease return null @@ -94,7 +95,7 @@ class ScanProductTask( private class ScanNoteProcessor : ProductCommandProcessor { override fun proceed( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { @@ -117,7 +118,7 @@ private class ScanWalletProcessor( var primaryCard: PrimaryCard? = null override fun proceed( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { @@ -125,7 +126,7 @@ private class ScanWalletProcessor( } private fun createMissingWalletsIfNeeded( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { @@ -159,13 +160,13 @@ private class ScanWalletProcessor( } private fun startLinkingForBackupIfNeeded( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId) - if ((card.backupStatus == Card.BackupStatus.NoBackup && card.wallets.isNotEmpty()) + if ((card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty()) && (activationInProgress || card.isSaltPay) ) { StartPrimaryCardLinkingTask().run(session) { linkingResult -> @@ -185,7 +186,7 @@ private class ScanWalletProcessor( } private fun deriveKeysIfNeeded( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { @@ -227,25 +228,46 @@ private class ScanWalletProcessor( } } - private suspend fun getBlockchainsToDerive(card: Card): List { + private suspend fun getBlockchainsToDerive(card: CardDTO): List { val userTokensRepository = userTokensRepository ?: return emptyList() - val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty { - mutableListOf( - BlockchainNetwork(Blockchain.Bitcoin, card), - BlockchainNetwork(Blockchain.Ethereum, card), - ) - } + val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card) + .toMutableList() + .ifEmpty { + mutableListOf( + BlockchainNetwork( + blockchain = Blockchain.Bitcoin, + card = card, + ), + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + card = card, + ), + ) + } if (card.settings.isHDWalletAllowed) { blockchainsToDerive.addAll( listOf( - BlockchainNetwork(Blockchain.Ethereum, card), - BlockchainNetwork(Blockchain.EthereumTestnet, card), + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + card = card, + ), + BlockchainNetwork( + blockchain = Blockchain.EthereumTestnet, + card = card, + ), ), ) } if (additionalBlockchainsToDerive != null) { - blockchainsToDerive.addAll(additionalBlockchainsToDerive.map { BlockchainNetwork(it, card) }) + blockchainsToDerive.addAll( + additionalBlockchainsToDerive.map { + BlockchainNetwork( + blockchain = it, + card = card, + ) + }, + ) } if (!card.useOldStyleDerivation) { blockchainsToDerive.removeAll( @@ -255,13 +277,18 @@ private class ScanWalletProcessor( Blockchain.RSK, Blockchain.Fantom, Blockchain.FantomTestnet, Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ).map { BlockchainNetwork(it, card) }, + ).map { + BlockchainNetwork( + blockchain = it, + card = card, + ) + }, ) } return blockchainsToDerive.distinct() } - private suspend fun collectDerivations(card: Card): Map> { + private suspend fun collectDerivations(card: CardDTO): Map> { val blockchains = getBlockchainsToDerive(card) val derivations = mutableMapOf>() @@ -287,14 +314,14 @@ private class ScanWalletProcessor( private class ScanTwinProcessor : ProductCommandProcessor { override fun proceed( - card: Card, + card: CardDTO, session: CardSession, callback: (result: CompletionResult) -> Unit, ) { ReadIssuerDataCommand().run(session) { readDataResult -> when (readDataResult) { is CompletionResult.Success -> { - val publicKey = card.getSingleWallet()?.publicKey + val publicKey = card.wallets.firstOrNull()?.publicKey if (publicKey == null) { val response = ScanResponse( card = card, @@ -332,7 +359,7 @@ private class ScanTwinProcessor : ProductCommandProcessor { } } -fun Card.getCurvesForNonCreatedWallets(): List { +fun CardDTO.getCurvesForNonCreatedWallets(): List { val curvesPresent = wallets.map { it.curve }.toSet() val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1) return curvesForNonCreatedWallets.toList() diff --git a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt index f05065bc8d..6644362bf5 100644 --- a/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt +++ b/app/src/main/java/com/tangem/tap/domain/termsOfUse/CardTou.kt @@ -3,8 +3,8 @@ package com.tangem.tap.domain.termsOfUse import android.content.res.Resources import android.net.Uri import androidx.core.os.ConfigurationCompat -import com.tangem.common.card.Card -import java.util.Locale +import com.tangem.domain.common.CardDTO +import java.util.* /** [REDACTED_AUTHOR] @@ -13,7 +13,7 @@ class CardTou { private val locale: Locale = ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)!! - fun getUrl(card: Card): Uri? { + fun getUrl(card: CardDTO): Uri? { val issuerName = card.issuer.name if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt index d3d5836701..28bcdc9c35 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt @@ -2,10 +2,11 @@ package com.tangem.tap.domain.tokens import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.CardDTO object CurrenciesRepository { fun getBlockchains( - cardFirmware: FirmwareVersion, + cardFirmware: CardDTO.FirmwareVersion, isTestNet: Boolean = false, ): List { val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) { @@ -26,6 +27,4 @@ object CurrenciesRepository { ) } } -} - - +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index 40b8389049..4d6646ed32 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -2,13 +2,13 @@ package com.tangem.tap.domain.tokens import android.content.Context import com.tangem.blockchain.common.DerivationStyle -import com.tangem.common.card.Card import com.tangem.common.services.Result +import com.tangem.domain.common.CardDTO +import com.tangem.domain.common.util.userWalletId import com.tangem.network.api.tangemTech.TangemTechService import com.tangem.network.api.tangemTech.UserTokensResponse import com.tangem.tap.common.AndroidFileReader import com.tangem.tap.domain.NoDataError -import com.tangem.tap.domain.extensions.getUserWalletId import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.models.Currency @@ -23,8 +23,8 @@ class UserTokensRepository( private val storageService: UserTokensStorageService, private val networkService: UserTokensNetworkService, ) { - suspend fun getUserTokens(card: Card): List { - val userId = card.getUserWalletId() + suspend fun getUserTokens(card: CardDTO): List { + val userId = card.userWalletId.stringValue if (DemoHelper.isDemoCardId(card.cardId)) { return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() } } @@ -36,25 +36,28 @@ class UserTokensRepository( return when (val networkResult = networkService.getUserTokens(userId)) { is Result.Success -> { val tokens = networkResult.data.tokens.mapNotNull { Currency.fromTokenResponse(it) } - storageService.saveUserTokens(card.getUserWalletId(), tokens.toUserTokensResponse()) + storageService.saveUserTokens(userId, tokens.toUserTokensResponse()) tokens.distinct() } + is Result.Failure -> { handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error) } } } - suspend fun saveUserTokens(card: Card, tokens: List) { + suspend fun saveUserTokens(card: CardDTO, tokens: List) { + val userId = card.userWalletId.stringValue val userTokens = tokens.toUserTokensResponse() - networkService.saveUserTokens(card.getUserWalletId(), userTokens) - storageService.saveUserTokens(card.getUserWalletId(), userTokens) + networkService.saveUserTokens(userId, userTokens) + storageService.saveUserTokens(userId, userTokens) } - suspend fun removeUserTokens(card: Card) { + suspend fun removeUserTokens(card: CardDTO) { + val userId = card.userWalletId.stringValue val userTokens = emptyList().toUserTokensResponse() - networkService.saveUserTokens(card.getUserWalletId(), userTokens) - storageService.saveUserTokens(card.getUserWalletId(), userTokens) + networkService.saveUserTokens(userId, userTokens) + storageService.saveUserTokens(userId, userTokens) } private fun List.toUserTokensResponse(): UserTokensResponse { @@ -66,8 +69,8 @@ class UserTokensRepository( ) } - suspend fun loadBlockchainsToDerive(card: Card): List { - val userId = card.getUserWalletId() + suspend fun loadBlockchainsToDerive(card: CardDTO): List { + val userId = card.userWalletId.stringValue val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks() if (DemoHelper.isDemoCardId(card.cardId)) { @@ -89,7 +92,7 @@ class UserTokensRepository( } private suspend fun handleGetUserTokensFailure( - card: Card, + card: CardDTO, userId: String, error: Throwable, ): List { @@ -107,7 +110,7 @@ class UserTokensRepository( } } - private suspend fun loadTokensOffline(card: Card, userId: String): List { + private suspend fun loadTokensOffline(card: CardDTO, userId: String): List { return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card) } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index c71e5afa53..5f36f9bd49 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -2,7 +2,7 @@ package com.tangem.tap.domain.tokens import com.squareup.moshi.JsonAdapter import com.tangem.Log -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.network.api.tangemTech.UserTokensResponse import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.FileReader @@ -28,7 +28,7 @@ class UserTokensStorageService( } @Deprecated("") - suspend fun getUserTokens(card: Card): List { + suspend fun getUserTokens(card: CardDTO): List { val blockchainNetworks = oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed) return blockchainNetworks.flatMap { it.toCurrencies() } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt index 36e376a08b..3b9558c303 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt @@ -4,8 +4,8 @@ import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager -import com.tangem.common.card.Card import com.tangem.common.extensions.calculateHashCode +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.derivationStyle @JsonClass(generateAdapter = true) @@ -15,16 +15,18 @@ data class BlockchainNetwork( val tokens: List ) { - constructor(blockchain: Blockchain, card: Card) : this( + constructor( + blockchain: Blockchain, + card: CardDTO, + ) : this( blockchain = blockchain, derivationPath = if (card.settings.isHDWalletAllowed) blockchain.derivationPath(card.derivationStyle)?.rawPath else null, - tokens = emptyList() + tokens = emptyList(), ) - fun updateTokens(tokens: List): BlockchainNetwork { return copy( - tokens = (this.tokens + tokens).distinct() + tokens = (this.tokens + tokens).distinct(), ) } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt index 95e8753b3d..3860732b7d 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt @@ -9,7 +9,6 @@ import com.tangem.domain.common.TwinsHelper import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask import com.tangem.operations.wallet.PurgeWalletCommand -import com.tangem.tap.domain.extensions.getSingleWallet class CreateFirstTwinWalletTask : CardSessionRunnable { override fun run( diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt index 8780d00055..cca9da3551 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -23,7 +23,7 @@ class CreateSecondTwinWalletTask( override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { val card = session.environment.card - val publicKey = card?.getSingleWallet()?.publicKey + val publicKey = card?.wallets?.firstOrNull()?.publicKey if (publicKey != null) { if (TwinsHelper.getTwinCardNumber(card.cardId) == TwinCardNumber.First) { callback(CompletionResult.Failure(WrongTwinCard(TwinCardNumber.Second))) @@ -63,4 +63,4 @@ class CreateSecondTwinWalletTask( } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index ad9df6d287..c29d226df4 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -6,9 +6,9 @@ import com.tangem.Message import com.tangem.blockchain.extensions.Result import com.tangem.common.CompletionResult import com.tangem.common.KeyPair -import com.tangem.common.card.Card import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.network.common.MoshiConverter import com.tangem.operations.wallet.CreateWalletResponse @@ -16,7 +16,7 @@ import com.tangem.tap.common.AssetReader import com.tangem.tap.tangemSdkManager class TwinCardsManager( - card: Card, + card: CardDTO, assetReader: AssetReader, ) { @@ -91,7 +91,6 @@ class TwinCardsManager( } private class Issuer( - val id: String, val privateKey: String, val publicKey: String, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt index e4ae0109bd..31d35c4a06 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/WriteProtectedIssuerDataTask.kt @@ -13,7 +13,6 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.operations.issuerAndUserData.ReadIssuerDataResponse import com.tangem.operations.issuerAndUserData.WriteIssuerDataCommand import com.tangem.operations.sign.SignHashCommand -import com.tangem.tap.domain.extensions.getSingleWallet class WriteProtectedIssuerDataTask( private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair, @@ -25,7 +24,7 @@ class WriteProtectedIssuerDataTask( ) { SignHashCommand( twinPublicKey.calculateSha256(), - session.environment.card!!.getSingleWallet()!!.publicKey + session.environment.card!!.wallets.first().publicKey, ) .run(session) { signResult -> when (signResult) { diff --git a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt index 66d09bdbf0..1e97beb6e0 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.demo -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse /** [REDACTED_AUTHOR] */ fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId) -fun Card.isDemoCard(): Boolean = DemoHelper.isDemoCardId(cardId) \ No newline at end of file +fun CardDTO.isDemoCard(): Boolean = DemoHelper.isDemoCardId(cardId) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index b421b69730..787e92f332 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.details.redux import com.tangem.blockchain.common.Wallet -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.domain.termsOfUse.CardTou @@ -30,7 +30,7 @@ sealed class DetailsAction : Action { object ScanCard : DetailsAction() - data class PrepareCardSettingsData(val card: Card) : DetailsAction() + data class PrepareCardSettingsData(val card: CardDTO) : DetailsAction() object ResetCardSettingsData : DetailsAction() sealed class ManageSecurity : DetailsAction() { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 88191c0ee4..4c2d72d2fe 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.details.redux import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.domain.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.common.util.userWalletId import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings @@ -13,7 +14,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.extensions.getUserWalletId import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction @@ -67,11 +67,12 @@ class DetailsMiddleware { scope.launch { when (val result = tangemSdkManager.scanCard()) { is CompletionResult.Success -> { - val card = result.data - if (card.getUserWalletId() == - store.state.globalState.scanResponse?.card?.getUserWalletId() - ) { - store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card)) + val scannedCard = result.data + val currentCardId = store.state.globalState.scanResponse?.card + ?.userWalletId + ?.stringValue + if (scannedCard.userWalletId.stringValue == currentCardId) { + store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(scannedCard)) } else { store.dispatchDialogShow( AppDialog.SimpleOkDialogRes( @@ -81,8 +82,7 @@ class DetailsMiddleware { ) } } - is CompletionResult.Failure -> { - } + is CompletionResult.Failure -> Unit } } } @@ -104,22 +104,20 @@ class DetailsMiddleware { is DetailsAction.ResetToFactory.Proceed -> { val card = store.state.detailsState.cardSettingsState?.card ?: return scope.launch { - val result = tangemSdkManager.resetToFactorySettings(card) - when (result) { + when (val result = tangemSdkManager.resetToFactorySettings(card.cardId)) { is CompletionResult.Success -> { Analytics.send(Settings.CardSettings.FactoryResetFinished()) store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) } + is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) - } + Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) } } } } } - else -> { /* no-op */ - } + else -> Unit } } } @@ -156,8 +154,7 @@ class DetailsMiddleware { } store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure) } - else -> { /* no-op */ - } + else -> Unit } } } @@ -171,8 +168,7 @@ class DetailsMiddleware { } } } - else -> { /* no-op */ - } + else -> Unit } } } @@ -186,4 +182,4 @@ class DetailsMiddleware { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index ef4a0aa975..225505dfde 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.details.redux -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemNote @@ -51,12 +51,12 @@ private fun handlePrepareScreen( scanResponse = action.scanResponse, wallets = action.wallets, cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card), - createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup, + createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, appCurrency = store.state.globalState.appCurrency, ) } -private fun handlePrepareCardSettingsScreen(card: Card, state: DetailsState): DetailsState { +private fun handlePrepareCardSettingsScreen(card: CardDTO, state: DetailsState): DetailsState { val cardSettingsState = CardSettingsState( cardInfo = card.toCardInfo(), manageSecurityState = prepareSecurityOptions(card), @@ -66,14 +66,16 @@ private fun handlePrepareCardSettingsScreen(card: Card, state: DetailsState): De return state.copy(cardSettingsState = cardSettingsState) } -private fun prepareSecurityOptions(card: Card): ManageSecurityState { +private fun prepareSecurityOptions(card: CardDTO): ManageSecurityState { val securityOption = when { card.isAccessCodeSet -> { SecurityOption.AccessCode } + card.isPasscodeSet == true -> { SecurityOption.PassCode } + else -> { SecurityOption.LongTap } @@ -94,7 +96,7 @@ private fun prepareSecurityOptions(card: Card): ManageSecurityState { ) } -private fun isResetToFactoryAllowedByCard(card: Card): Boolean { +private fun isResetToFactoryAllowedByCard(card: CardDTO): Boolean { val notAllowedByAnyWallet = card.wallets.any { it.settings.isPermanent } val notAllowedByCard = notAllowedByAnyWallet || (card.isWalletDataSupported && (!card.isTangemNote && !card.settings.isBackupAllowed)) || @@ -114,7 +116,8 @@ private fun handleEraseWallet( } private fun handleSecurityAction( - action: DetailsAction.ManageSecurity, state: DetailsState, + action: DetailsAction.ManageSecurity, + state: DetailsState, ): DetailsState { return when (action) { is DetailsAction.ManageSecurity.SelectOption -> { @@ -144,7 +147,8 @@ private fun handleSecurityAction( } private fun handlePrivacyAction( - action: DetailsAction.AppSettings, state: DetailsState, + action: DetailsAction.AppSettings, + state: DetailsState, ): DetailsState { return when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { @@ -157,7 +161,8 @@ private fun handlePrivacyAction( } private fun prepareAllowedSecurityOptions( - card: Card?, currentSecurityOption: SecurityOption?, + card: CardDTO?, + currentSecurityOption: SecurityOption?, ): EnumSet { val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap) @@ -173,7 +178,7 @@ private fun prepareAllowedSecurityOptions( return allowedSecurityOptions } -private fun Card.toCardInfo(): CardInfo { +private fun CardDTO.toCardInfo(): CardInfo { val cardId = this.cardId.chunked(4).joinToString(separator = " ") val issuer = this.issuer.name val signedHashes = this.signedHashesCount() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 8e02856097..07c0847213 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.details.redux import android.net.Uri import com.tangem.blockchain.common.Wallet -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.FiatCurrency @@ -20,13 +20,13 @@ data class DetailsState( val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, val appCurrency: FiatCurrency = FiatCurrency.Default, - val saveWallets: Boolean = true, - val saveAccessCodes: Boolean = true, + val saveWallets: Boolean = false, + val saveAccessCodes: Boolean = false, ) : StateType { // if you do not delegate - the application crashes on startup, // because twinCardsState has not been created yet - val twinCardsState: TwinCardsState by ReadOnlyProperty { thisRef, property -> + val twinCardsState: TwinCardsState by ReadOnlyProperty { _, _ -> store.state.twinCardsState } @@ -42,7 +42,7 @@ data class CardInfo( data class CardSettingsState( val cardInfo: CardInfo, - val card: Card, + val card: CardDTO, val manageSecurityState: ManageSecurityState?, val resetCardAllowed: Boolean, val resetConfirmed: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index ed2c68c8d6..7818f367b3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.WalletManager -import com.tangem.common.card.Card import com.tangem.common.extensions.guard +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.withMainContext @@ -240,7 +240,7 @@ class WalletConnectMiddleware { handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain) } - private fun getAvailableBlockchains(card: Card, walletState: WalletState): List { + private fun getAvailableBlockchains(card: CardDTO, walletState: WalletState): List { return walletState.currencies.filter { it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm() }.map { it.blockchain } diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt index 530a4afb78..ee0700f5b5 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.disclaimer.redux import android.net.Uri -import com.tangem.common.card.Card import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isSaltPay import org.rekotlin.StateType @@ -22,7 +22,7 @@ sealed class DisclaimerType( companion object { fun get(scanResponse: ScanResponse): DisclaimerType = get(scanResponse.card) - fun get(card: Card): DisclaimerType = when { + fun get(card: CardDTO): DisclaimerType = when { card.isSaltPay -> SaltPay else -> Tangem } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 0a276dff4f..6b71aee0f7 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -14,4 +14,4 @@ sealed class HomeAction : Action { data class GoToShop(val userCountryCode: String?) : HomeAction() data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction() -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index c3c0145ec8..1d94184e98 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.home.redux -import com.tangem.common.card.Card import com.tangem.common.core.TangemError import com.tangem.common.services.Result +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withIOContext import com.tangem.domain.common.extensions.withMainContext @@ -45,7 +45,6 @@ import com.tangem.wallet.R import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action -import org.rekotlin.DispatchFunction import org.rekotlin.Middleware class HomeMiddleware { @@ -56,22 +55,23 @@ class HomeMiddleware { } } -private val homeMiddleware: Middleware = { dispatch, state -> +private val homeMiddleware: Middleware = { _, _ -> { next -> { action -> - handleHomeAction(state, action, dispatch) + handleHomeAction(action) next(action) } } } -private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) { +private fun handleHomeAction(action: Action) { when (action) { is HomeAction.Init -> { store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.ExchangeManager.Init) store.dispatch(GlobalAction.FetchUserCountry) } + is HomeAction.ShouldScanCardOnResume -> { if (action.shouldScanCard) { store.dispatch(HomeAction.ShouldScanCardOnResume(false)) @@ -123,7 +123,7 @@ private fun checkForUnfinishedBackupForSaltPay( return } - fun isTheSamePrimaryCard(card: Card): Boolean { + fun isTheSamePrimaryCard(card: CardDTO): Boolean { return backupService.primaryCardId?.let { it == card.cardId } ?: false } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 2917a052fe..f8996b9ea9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.onboarding import com.tangem.domain.common.ProductType import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.navigation.AppScreen -import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.preferencesStorage /** @@ -23,7 +22,7 @@ class OnboardingHelper { cardInfoStorage.isActivationInProgress(cardId) } } - response.card.hasWallets() -> cardInfoStorage.isActivationInProgress(cardId) + response.card.wallets.isNotEmpty() -> cardInfoStorage.isActivationInProgress(cardId) else -> true } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 5ecccaa431..6f17c03078 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -22,7 +22,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE @@ -79,7 +78,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch } is OnboardingNoteAction.DetermineStepOfScreen -> { val step = when { - !card.hasWallets() -> OnboardingNoteStep.CreateWallet + card.wallets.isEmpty() -> OnboardingNoteStep.CreateWallet noteState.walletBalance.balanceIsToppedUp() -> OnboardingNoteStep.Done else -> OnboardingNoteStep.TopUpWallet } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 45c4a48fc8..acd4013518 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -12,7 +12,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope @@ -21,7 +20,6 @@ import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action -import org.rekotlin.DispatchFunction import org.rekotlin.Middleware class OnboardingOtherCardsMiddleware { @@ -30,16 +28,16 @@ class OnboardingOtherCardsMiddleware { } } -private val onboardingOtherCardsMiddleware: Middleware = { dispatch, state -> +private val onboardingOtherCardsMiddleware: Middleware = { dispatch, _ -> { next -> { action -> - handleOtherCardsAction(action, dispatch) + handleOtherCardsAction(action) next(action) } } } -private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { +private fun handleOtherCardsAction(action: Action) { if (action !is OnboardingOtherCardsAction) return val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager ?: return @@ -60,7 +58,7 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { } is OnboardingOtherCardsAction.DetermineStepOfScreen -> { val step = when { - !card.hasWallets() -> OnboardingOtherCardsStep.CreateWallet + card.wallets.isEmpty() -> OnboardingOtherCardsStep.CreateWallet else -> OnboardingOtherCardsStep.Done } store.dispatch((OnboardingOtherCardsAction.SetStepOfScreen(step))) @@ -87,21 +85,32 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { val updatedResponse = onboardingManager.scanResponse.copy( card = result.data.card, ) + val updatedCard = updatedResponse.card onboardingManager.scanResponse = updatedResponse - onboardingManager.activationStarted(updatedResponse.card.cardId) + onboardingManager.activationStarted(updatedCard.cardId) val primaryBlockchain = updatedResponse.getBlockchain() val blockchainNetworks = if (primaryBlockchain != Blockchain.Unknown) { val primaryToken = updatedResponse.getPrimaryToken() val blockchainNetwork = - BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens( - listOfNotNull(primaryToken), + BlockchainNetwork( + blockchain = primaryBlockchain, + card = updatedCard, ) + .updateTokens( + listOfNotNull(primaryToken), + ) listOf(blockchainNetwork) } else { listOf( - BlockchainNetwork(Blockchain.Bitcoin, updatedResponse.card), - BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card), + BlockchainNetwork( + blockchain = Blockchain.Bitcoin, + card = updatedCard, + ), + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + card = updatedCard, + ), ) } @@ -126,5 +135,9 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { withMainContext { store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) } } } + is OnboardingOtherCardsAction.Confetti.Hide, + is OnboardingOtherCardsAction.SetArtworkUrl, + is OnboardingOtherCardsAction.Confetti.Show, + -> Unit } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index aa6de330d6..7233956925 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -150,6 +150,13 @@ private fun handle(action: Action, dispatch: DispatchFunction) { finishCardActivation() postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) } } + + TwinCardsStep.None, + TwinCardsStep.Warning, + TwinCardsStep.CreateFirstWallet, + TwinCardsStep.CreateSecondWallet, + TwinCardsStep.CreateThirdWallet, + -> Unit } } is TwinCardsAction.Wallet.LaunchFirstStep -> { @@ -299,4 +306,4 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index dfe53282ed..31b85f86e6 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -2,8 +2,8 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult -import com.tangem.common.card.Card import com.tangem.common.extensions.ifNotNull +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.extensions.withMainContext @@ -18,7 +18,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction @@ -76,19 +75,23 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: // it's possible when found unfinished backup for standard Wallet cards store.dispatch(OnboardingWalletAction.ResumeBackup) } - card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup -> { + + card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> { store.dispatch(OnboardingWalletAction.ResumeBackup) } - card.hasWallets() && card.backupStatus?.isActive == true -> { + + card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> { when { // check for unfinished backup for saltPay cards. See more scanResponse.isSaltPay() && backupService.hasIncompletedBackup -> { store.dispatch(OnboardingWalletAction.ResumeBackup) } + scanResponse.isSaltPay() -> { store.dispatch(OnboardingWalletAction.GetToSaltPayStep) store.dispatch(BackupAction.FinishBackup) } + else -> store.dispatch(BackupAction.FinishBackup) } } @@ -125,8 +128,14 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: ) onboardingManager.scanResponse = updatedResponse val blockchainNetworks = listOf( - BlockchainNetwork(Blockchain.Bitcoin, result.data.card), - BlockchainNetwork(Blockchain.Ethereum, result.data.card), + BlockchainNetwork( + blockchain = Blockchain.Bitcoin, + card = result.data.card, + ), + BlockchainNetwork( + blockchain = Blockchain.Ethereum, + card = result.data.card, + ), ) store.dispatch( WalletAction.MultiWallet.SaveCurrencies( @@ -193,7 +202,7 @@ private fun updateScanResponseAfterBackup( val card = if (backupState.backupCardsNumber > 0) { val cardsCount = backupState.backupCardsNumber scanResponse.card.copy( - backupStatus = Card.BackupStatus.Active(cardCount = cardsCount), + backupStatus = CardDTO.BackupStatus.Active(cardCount = cardsCount), isAccessCodeSet = true, ) } else { @@ -366,9 +375,10 @@ private fun startCardActivation(scanResponse: ScanResponse) { * Standard Wallet cards finish activation at BackupAction.FinishBackup * SaltPay cards finish activation at OnboardingWalletAction.FinishOnboarding */ -internal fun finishCardActivation(backupState: BackupState, card: Card?) { +internal fun finishCardActivation(backupState: BackupState, card: CardDTO?) { (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) - .distinct().filterNotNull() + .distinct() + .filterNotNull() .forEach { cardId -> preferencesStorage.usedCardsPrefStorage.activationFinished(cardId) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt index 4ef4b407d2..04933d0a46 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt @@ -11,6 +11,7 @@ import com.tangem.common.extensions.guard import com.tangem.common.extensions.isZero import com.tangem.common.services.Result import com.tangem.domain.common.extensions.successOr +import com.tangem.domain.common.util.UserWalletId import com.tangem.network.api.paymentology.AttestationResponse import com.tangem.network.api.paymentology.PaymentologyApiService import com.tangem.network.api.paymentology.RegisterKYCRequest @@ -20,7 +21,6 @@ import com.tangem.network.api.paymentology.RegistrationResponse import com.tangem.network.api.paymentology.tryExtractError import com.tangem.operations.attestation.AttestWalletKeyResponse import com.tangem.tap.common.extensions.safeUpdate -import com.tangem.tap.domain.extensions.UserWalletId import com.tangem.tap.domain.getFirstToken import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError import java.math.BigDecimal @@ -123,7 +123,7 @@ class SaltPayActivationManager( } suspend fun claim(amountToClaim: BigDecimal, signer: TransactionSigner): Result { - val result = gnosisRegistrator.transferFrom(amountToClaim, signer).successOr { + gnosisRegistrator.transferFrom(amountToClaim, signer).successOr { return Result.Failure(SaltPayActivationError.ClaimTransactionFailed) } return Result.Success(Unit) @@ -192,4 +192,4 @@ class KYCUrlProvider( .appendQueryParameter(kycProvider.externalIdParameterKey, kycRefId) .build().toString() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index da468c2636..13811d5ed0 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -11,9 +11,9 @@ import com.tangem.blockchain.common.TransactionError import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError import com.tangem.common.services.Result +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE @@ -150,7 +150,7 @@ private fun sendTransaction( feeAmount: Amount, destinationAddress: String, transactionExtras: TransactionExtrasState, - card: Card, + card: CardDTO, externalTransactionData: ExternalTransactionData?, dispatch: (Action) -> Unit, ) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt index 06e3cbae64..4c5dd07560 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt @@ -1,8 +1,6 @@ package com.tangem.tap.features.wallet.models -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle -import com.tangem.blockchain.common.Token import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId @@ -11,6 +9,8 @@ import com.tangem.network.api.tangemTech.TokenResponse import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.tokens.redux.TokenWithBlockchain +import com.tangem.blockchain.common.Blockchain as SdkBlockchain +import com.tangem.blockchain.common.Token as SdkToken sealed interface Currency { val coinId: String? @@ -18,7 +18,7 @@ sealed interface Currency { is Blockchain -> blockchain.toCoinId() is Token -> token.id } - val blockchain: com.tangem.blockchain.common.Blockchain + val blockchain: SdkBlockchain val currencySymbol: CryptoCurrencyName val derivationPath: String? val currencyName: String @@ -33,15 +33,15 @@ sealed interface Currency { } data class Token( - val token: com.tangem.blockchain.common.Token, - override val blockchain: com.tangem.blockchain.common.Blockchain, + val token: SdkToken, + override val blockchain: SdkBlockchain, override val derivationPath: String?, ) : Currency { override val currencySymbol = token.symbol } data class Blockchain( - override val blockchain: com.tangem.blockchain.common.Blockchain, + override val blockchain: SdkBlockchain, override val derivationPath: String?, ) : Currency { override val currencySymbol: CryptoCurrencyName = blockchain.currency @@ -72,7 +72,7 @@ sealed interface Currency { companion object { fun fromBlockchainNetwork( blockchainNetwork: BlockchainNetwork, - token: com.tangem.blockchain.common.Token? = null, + token: SdkToken? = null, ): Currency { return if (token != null) { Token( @@ -115,7 +115,7 @@ sealed interface Currency { ?: return null return when { tokenResponse.contractAddress != null -> Token( - com.tangem.blockchain.common.Token( + token = SdkToken( name = tokenResponse.name, symbol = tokenResponse.symbol, contractAddress = tokenResponse.contractAddress!!, @@ -148,8 +148,8 @@ fun List.toBlockchainNetworks(): List { return this.filter { it.isBlockchain() }.map { BlockchainNetwork(it.blockchain, it.derivationPath, getTokens(it)) } } -private fun List.getTokens(currency: Currency): List { +fun List.getTokens(currency: Currency): List { return this .filter { it.isToken() && it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath } .mapNotNull { if (it is Currency.Token) it.token else null } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 02e6073066..fc947d90fd 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -7,9 +7,8 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage @@ -70,7 +69,7 @@ sealed class WalletAction : Action { data class AddToken(val token: Token, val blockchain: BlockchainNetwork, val save: Boolean) : MultiWallet() data class SaveCurrencies( - val blockchainNetworks: List, val card: Card? = null, + val blockchainNetworks: List, val card: CardDTO? = null, ) : MultiWallet() data class TokenLoaded( @@ -123,9 +122,9 @@ sealed class WalletAction : Action { object Failure : WalletAction() } - class LoadCardInfo(val card: Card) : WalletAction() + class LoadCardInfo(val card: CardDTO) : WalletAction() - data class LoadArtwork(val card: Card, val artworkId: String?) : WalletAction() { + data class LoadArtwork(val card: CardDTO, val artworkId: String?) : WalletAction() { data class Success(val artwork: Artwork) : WalletAction() object Failure : WalletAction() } @@ -134,10 +133,6 @@ sealed class WalletAction : Action { data class Send(val amount: Amount? = null) : WalletAction() - object EmptyField : WalletAction(), ErrorAction { - override val error = TapError.PayIdEmptyField - } - data class CopyAddress(val address: String, val context: Context) : WalletAction() { object Success : WalletAction(), NotificationAction { override val messageResource = R.string.wallet_notification_address_copied @@ -186,7 +181,7 @@ sealed class WalletAction : Action { data class SetWalletRent( val wallet: Wallet, val minRent: String, - val rentExempt: String + val rentExempt: String, ) : WalletAction() data class RemoveWalletRent(val wallet: Wallet) : WalletAction() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 112c3a0afb..109d44ad31 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.SignatureCountValidator import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.extensions.dispatchOnMain @@ -13,10 +13,8 @@ import com.tangem.tap.common.extensions.isGreaterThan import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.domain.extensions.getSingleWallet import com.tangem.tap.domain.extensions.hasSignedHashes import com.tangem.tap.domain.extensions.isMultiwalletAllowed -import com.tangem.tap.domain.extensions.remainingSignatures import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity @@ -108,8 +106,8 @@ class WarningsMiddleware { } } - private fun showWarningLowRemainingSignaturesIfNeeded(card: Card) { - val remainingSignatures = card.remainingSignatures + private fun showWarningLowRemainingSignaturesIfNeeded(card: CardDTO) { + val remainingSignatures = card.wallets.firstOrNull()?.remainingSignatures if (remainingSignatures != null && remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING ) { @@ -159,7 +157,7 @@ class WarningsMiddleware { val validator = store.state.walletState.walletManagers.firstOrNull() as? SignatureCountValidator scope.launch { - val signedHashes = card.getSingleWallet()?.totalSignedHashes ?: 0 + val signedHashes = card.wallets.firstOrNull()?.totalSignedHashes ?: 0 val result = validator?.validateSignatureCount(signedHashes) withContext(Dispatchers.Main) { when (result) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index d4172e31ef..956fe058dc 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.wallet.ui.wallet import android.widget.Button import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.analytics.Analytics @@ -54,8 +54,8 @@ class MultiWalletView : WalletView() { private fun setupWalletCardNumber(binding: FragmentWalletBinding) = with(binding) { val card = store.state.globalState.scanResponse?.card - if (card?.backupStatus is Card.BackupStatus.Active) { - val cardCount = (card.backupStatus as Card.BackupStatus.Active).cardCount + 1 + if (card?.backupStatus is CardDTO.BackupStatus.Active) { + val cardCount = (card.backupStatus as CardDTO.BackupStatus.Active).cardCount + 1 tvTwinCardNumber.show() tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, cardCount) } else { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt index a7f7b9aec2..939121bda7 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt @@ -1,6 +1,6 @@ package com.tangem.tap.network.exchangeServices -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.models.Currency @@ -9,7 +9,7 @@ import com.tangem.tap.features.wallet.models.Currency [REDACTED_AUTHOR] */ class CardExchangeRules( - val cardProvider: () -> Card?, + val cardProvider: () -> CardDTO?, ) : ExchangeRules { override fun featureIsSwitchedOn(): Boolean { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 04c92fc542..3f61d82801 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -7,7 +7,7 @@ import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.GlobalAction @@ -86,8 +86,9 @@ class CurrencyExchangeManager( } } +@Suppress("unused") suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( - card: Card, + card: CardDTO, walletManager: EthereumWalletManager, token: Token, ) { diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index c161b935fb..11cfdee61f 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -1,6 +1,6 @@ package com.tangem.tap.proxy -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TangemSdkManager @@ -20,7 +20,7 @@ class AppStateHolder { var mainStore: Store? = null var tangemSdkManager: TangemSdkManager? = null - fun getActualCard(): Card? { + fun getActualCard(): CardDTO? { return scanResponse?.card } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 2832cd6faf..8856cdc80d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -4,15 +4,15 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.common.card.Card +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.common.util.userWalletId import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.NativeToken import com.tangem.lib.crypto.models.NonNativeToken -import com.tangem.tap.domain.extensions.getUserWalletId import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction @@ -54,7 +54,7 @@ class UserWalletManagerImpl( } override fun getWalletId(): String { - return appStateHolder.getActualCard()?.getUserWalletId() ?: "" + return appStateHolder.getActualCard()?.userWalletId?.stringValue ?: "" } override suspend fun isTokenAdded(currency: Currency): Boolean { @@ -100,7 +100,7 @@ class UserWalletManagerImpl( } } - private fun addNativeTokenToWalletAction(token: NativeToken, card: Card): Action { + private fun addNativeTokenToWalletAction(token: NativeToken, card: CardDTO): Action { val blockchain = Blockchain.fromNetworkId(token.networkId) val scanResponse = appStateHolder.scanResponse if (blockchain != null && scanResponse != null) { diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 1a730aa925..f9ac9274ec 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -58,11 +58,10 @@ object Versions { // endregion Other libraries // region Tangem - const val tangemBlockchainSdk = "develop-140" - - // const val tangemBlockchainSdk = "0.0.1" - const val tangemCardSgk = "develop-169" - // const val tangemCardSgk = "0.0.1" + const val tangemBlockchainSdk = "0.0.1" + // const val tangemBlockchainSdk = "develop-140" + const val tangemCardSgk = "0.0.1" + // const val tangemCardSgk = "develop-165" // endregion Tangem // region Testing diff --git a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt index 56c6979d5e..ad0538d7a5 100644 --- a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt +++ b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt @@ -2,7 +2,6 @@ package com.tangem.domain.common import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve import com.tangem.common.card.WalletData import com.tangem.common.extensions.ByteArrayKey @@ -12,7 +11,6 @@ import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain import com.tangem.domain.common.TapWorkarounds.isSaltPay import com.tangem.domain.common.TapWorkarounds.isSaltPayVisa import com.tangem.domain.common.TapWorkarounds.isSaltPayWallet -import com.tangem.domain.common.TapWorkarounds.isTangemNote import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.CommandResponse @@ -23,7 +21,7 @@ import com.tangem.operations.derivation.ExtendedPublicKeysMap [REDACTED_AUTHOR] */ data class ScanResponse( - val card: Card, + val card: CardDTO, val productType: ProductType, val walletData: WalletData?, val secondTwinPublicKey: String? = null, @@ -67,20 +65,22 @@ data class ScanResponse( return hasDerivation(blockchain, DerivationPath(rawDerivationPath)) } - fun hasDerivation(blockchain: Blockchain, derivationPath: DerivationPath): Boolean { + private fun hasDerivation(blockchain: Blockchain, derivationPath: DerivationPath): Boolean { val isTestnet = card.isTestCard || blockchain.isTestnet() return when { Blockchain.secp256k1Blockchains(isTestnet).contains(blockchain) -> { hasDerivation(EllipticCurve.Secp256k1, derivationPath) } + Blockchain.ed25519OnlyBlockchains(isTestnet).contains(blockchain) -> { hasDerivation(EllipticCurve.Ed25519, derivationPath) } + else -> false } } - fun hasDerivation(curve: EllipticCurve, derivationPath: DerivationPath): Boolean { + private fun hasDerivation(curve: EllipticCurve, derivationPath: DerivationPath): Boolean { val foundWallet = card.wallets.firstOrNull { it.curve == curve } ?: return false val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false @@ -93,12 +93,4 @@ enum class ProductType { Note, Twins, Wallet, SaltPay } -val Card.productType: ProductType - get() = when { - isTangemTwins -> ProductType.Twins - isTangemNote -> ProductType.Note - isSaltPay -> ProductType.SaltPay - else -> ProductType.Wallet - } - typealias KeyWalletPublicKey = ByteArrayKey \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index 69ec26308b..21fa7e169f 100644 --- a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -17,18 +17,32 @@ object TapWorkarounds { return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER } - val Card.isStart2Coin: Boolean + val CardDTO.isTangemTwins: Boolean + get() = TwinsHelper.getTwinCardNumber(cardId) != null + + //TODO: replace by reading files from a card + val CardDTO.isTangemNote: Boolean + get() = tangemNoteBatches.contains(batchId) + + val CardDTO.isStart2Coin: Boolean get() = isStart2CoinIssuer(issuer.name) - val Card.isTestCard: Boolean + val CardDTO.isSaltPay: Boolean + get() = isSaltPayVisa || isSaltPayWallet + + val CardDTO.isSaltPayVisa: Boolean + get() = SaltPayWorkaround.isVisaBatchId(batchId) + + val CardDTO.isSaltPayWallet: Boolean + get() = SaltPayWorkaround.isWalletCardId(cardId) + + val CardDTO.isTestCard: Boolean get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) - val Card.useOldStyleDerivation: Boolean + val CardDTO.useOldStyleDerivation: Boolean get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" - - val Card.derivationStyle: DerivationStyle? + val CardDTO.derivationStyle: DerivationStyle? get() = if (!settings.isHDWalletAllowed) { - Int.MAX_VALUE null } else if (useOldStyleDerivation) { DerivationStyle.LEGACY @@ -36,15 +50,18 @@ object TapWorkarounds { DerivationStyle.NEW } - val Card.isNotSupportedInThatRelease: Boolean + val CardDTO.isExcluded: Boolean + get() { + val excludedBatch = excludedBatches.contains(batchId) + val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) + return excludedBatch || excludedIssuerName + } + + val CardDTO.isNotSupportedInThatRelease: Boolean get() = false - val Card.isTangemTwins: Boolean - get() = TwinsHelper.getTwinCardNumber(cardId) != null - - //TODO: replace by reading files from a card - val Card.isTangemNote: Boolean - get() = tangemNoteBatches.contains(batchId) + fun CardDTO.getTangemNoteBlockchain(): Blockchain? = + tangemNoteBatches[batchId] ?: if (isSaltPay) Blockchain.Gnosis else null private val tangemNoteBatches = mapOf( "AB01" to Blockchain.Bitcoin, @@ -63,24 +80,6 @@ object TapWorkarounds { fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null - - val Card.isSaltPay: Boolean - get() = isSaltPayVisa || isSaltPayWallet - - val Card.isSaltPayVisa: Boolean - get() = SaltPayWorkaround.isVisaBatchId(batchId) - - val Card.isSaltPayWallet: Boolean - get() = SaltPayWorkaround.isWalletCardId(cardId) - - - val Card.isExcluded: Boolean - get() { - val excludedBatch = excludedBatches.contains(batchId) - val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) - return excludedBatch || excludedIssuerName - } - private val excludedBatches = listOf( "0027", "0030", diff --git a/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt b/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt index 23485d8340..a1ded44422 100644 --- a/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt +++ b/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt @@ -1,6 +1,5 @@ package com.tangem.domain.common -import com.tangem.common.card.Card import com.tangem.crypto.CryptoUtils class TwinsHelper { @@ -57,14 +56,14 @@ enum class TwinCardNumber(val number: Int) { } @Deprecated("Use ScanResponse.isTangemTwin") -fun Card.isTangemTwin(): Boolean { +fun CardDTO.isTangemTwin(): Boolean { return TwinsHelper.getTwinCardNumber(cardId) != null } -fun Card.getTwinCardNumber(): TwinCardNumber? { +fun CardDTO.getTwinCardNumber(): TwinCardNumber? { return TwinsHelper.getTwinCardNumber(this.cardId) } -fun Card.getTwinCardIdForUser(): String { +fun CardDTO.getTwinCardIdForUser(): String { return TwinsHelper.getTwinCardIdForUser(this.cardId) } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index acc0fc7bf0..bd7a3d7643 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -1,9 +1,9 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isTestCard /** @@ -12,11 +12,12 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard val FirmwareVersion.Companion.SolanaTokensAvailable get() = FirmwareVersion(4, 52) -fun Card.supportedBlockchains(): List { +fun CardDTO.supportedBlockchains(): List { val supportedBlockchains = when { firmwareVersion < FirmwareVersion.MultiWalletAvailable -> { Blockchain.fromCurve(EllipticCurve.Secp256k1) } + else -> { (Blockchain.fromCurve(EllipticCurve.Secp256k1) + Blockchain.fromCurve(EllipticCurve.Ed25519)).distinct() } @@ -26,7 +27,7 @@ fun Card.supportedBlockchains(): List { .filter { it.isSupportedInApp() } } -fun Card.supportedTokens(): List { +fun CardDTO.supportedTokens(): List { val tokensSupportedByBlockchain = supportedBlockchains().filter { it.canHandleTokens() }.toMutableList() val tokensSupportedByCard = when { firmwareVersion >= FirmwareVersion.SolanaTokensAvailable -> tokensSupportedByBlockchain @@ -41,10 +42,10 @@ fun Card.supportedTokens(): List { return filtered } -fun Card.canHandleBlockchain(blockchain: Blockchain): Boolean { +fun CardDTO.canHandleBlockchain(blockchain: Blockchain): Boolean { return this.supportedBlockchains().contains(blockchain) } -fun Card.canHandleToken(blockchain: Blockchain): Boolean { +fun CardDTO.canHandleToken(blockchain: Blockchain): Boolean { return this.supportedTokens().contains(blockchain) } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt b/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt index 4c8bedcd22..1d51a46d2c 100644 --- a/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt +++ b/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt @@ -10,8 +10,8 @@ class UserWalletId( ) { val value = stringValue.hexToBytes() - constructor(walletPublicKey: ByteArray) : this( - stringValue = calculateUserWalletId(walletPublicKey).toHexString(), + constructor(walletPublicKey: ByteArray?) : this( + stringValue = walletPublicKey?.let { calculateUserWalletId(it).toHexString() } ?: "", ) override fun equals(other: Any?): Boolean { diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index 28b821f2ab..6dde5b01c6 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -2,10 +2,10 @@ package com.tangem.domain.features.addCustomToken.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle -import com.tangem.common.card.Card import com.tangem.common.json.MoshiJsonConverter import com.tangem.domain.AddCustomTokenError import com.tangem.domain.DomainWrapped +import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.extensions.isSupportedInApp import com.tangem.domain.common.extensions.supportedBlockchains @@ -133,7 +133,7 @@ data class AddCustomTokenState( null } - fun reset(card: Card): AddCustomTokenState { + fun reset(card: CardDTO): AddCustomTokenState { return this.copy( appSavedCurrencies = null, onTokenAddCallback = null, @@ -159,7 +159,7 @@ data class AddCustomTokenState( .getConvertedData() } - fun getNetworks(card: Card, type: CustomTokenType): List { + fun getNetworks(card: CardDTO, type: CustomTokenType): List { return getNetworksList(card, type) } @@ -189,7 +189,7 @@ data class AddCustomTokenState( }.derivationPath(derivationStyleToUse) } - internal fun createFormFields(card: Card, type: CustomTokenType): List> { + internal fun createFormFields(card: CardDTO, type: CustomTokenType): List> { return listOf( TokenField(ContractAddress), TokenBlockchainField(Network, getNetworksList(card, type)), @@ -204,7 +204,7 @@ data class AddCustomTokenState( * Serves to determine the networks (blockchains & tokens) that can be selected by Form.Networks. * Blockchain.Unknown - is the default selection */ - private fun getNetworksList(card: Card, type: CustomTokenType): List { + private fun getNetworksList(card: CardDTO, type: CustomTokenType): List { val evmBlockchains = Blockchain.values() .filter { it.isEvm() } .filter { card.isTestCard == it.isTestnet() } @@ -239,7 +239,7 @@ data class AddCustomTokenState( ) } - private fun getSupportedDerivations(card: Card): List { + private fun getSupportedDerivations(card: CardDTO): List { val evmBlockchains = Blockchain.values() .filter { card.isTestCard == it.isTestnet() && it.isEvm() } .filter { it.isSupportedInApp() } From e72bf276da8ecbd7129d3b3a440b0519f4387679 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Nov 2022 18:08:46 +0300 Subject: [PATCH 06/16] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 10 +- .../com/tangem/tap/common/extensions/Store.kt | 6 + .../com/tangem/tap/common/redux/AppReducer.kt | 2 + .../com/tangem/tap/common/redux/AppState.kt | 4 +- .../redux/navigation/NavigationMiddleware.kt | 33 ++- .../redux/navigation/NavigationState.kt | 2 +- .../com/tangem/tap/domain/TangemSdkManager.kt | 15 ++ .../com/tangem/tap/domain/model/UserWallet.kt | 34 +++ .../tap/domain/scanCard/ScanCardProcessor.kt | 233 ++++++++++++++++++ .../userWalletList/UserWalletsListManager.kt | 29 +++ .../di/UserWalletsListProvider.kt | 8 + .../DummyUserWalletsListManager.kt | 58 +++++ .../welcome/redux/WelcomeMiddleware.kt | 90 +++++++ .../features/welcome/redux/WelcomeReducer.kt | 30 +++ .../features/welcome/redux/WelcomeState.kt | 7 +- .../features/welcome/ui/WelcomeFragment.kt | 3 +- .../features/welcome/ui/WelcomeScreenState.kt | 4 +- .../features/welcome/ui/WelcomeViewModel.kt | 21 +- 18 files changed, 568 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 05e9eed54d..f89ccbeb0a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -18,6 +18,7 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.IntentHandler import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction @@ -25,6 +26,8 @@ import com.tangem.tap.common.shop.GooglePayService import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import com.tangem.tap.domain.userWalletList.di.provideDummyImplementation import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.wallet.R @@ -40,6 +43,7 @@ import kotlin.coroutines.CoroutineContext lateinit var tangemSdk: TangemSdk lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService +lateinit var userWalletsListManager: UserWalletsListManager var notificationsHandler: NotificationsHandler? = null private val coroutineContext: CoroutineContext @@ -73,6 +77,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac tangemSdkManager = TangemSdkManager(tangemSdk, this) appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(tangemSdk, this) + userWalletsListManager = UserWalletsListManager.provideDummyImplementation() store.dispatch( ShopAction.CheckIfGooglePayAvailable( @@ -82,7 +87,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun systemActions() { - WindowCompat.setDecorFitsSystemWindows(window, false) val windowInsetsController = WindowInsetsControllerCompat(window, binding.root) @@ -106,7 +110,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted val shopOpened = store.state.shopState.total != null if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) + val navigateTo = if (userWalletsListManager.hasSavedUserWallets) AppScreen.Welcome else AppScreen.Home + + store.dispatchOnMain(NavigationAction.NavigateTo(navigateTo)) } intentHandler.handleIntent(intent) } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index d88560669e..1919cd3dbf 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError +import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers @@ -26,6 +27,11 @@ fun Store<*>.dispatchNotification(resId: Int) { dispatchOnMain(GlobalAction.ShowNotification(resId)) } +@Suppress("unused") // receiver type +fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) { + // TODO: Load tokens for selected user wallet. Will be created in further MRs +} + fun Store<*>.dispatchToastNotification(resId: Int) { dispatchOnMain(GlobalAction.ShowToastNotification(resId)) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index a14cdb9925..6e55404733 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -15,6 +15,7 @@ import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.reducers.WalletReducer import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.features.welcome.redux.WelcomeReducer import org.rekotlin.Action fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState { @@ -36,6 +37,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) tokensState = TokensReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), shopState = ShopReducer.reduce(action, state.shopState), + welcomeState = WelcomeReducer.reduce(action, state), ) } 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 a158f2c872..b8b4bf689a 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 @@ -34,8 +34,9 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware -import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.features.walletSelector.redux.WalletSelectorState +import com.tangem.tap.features.welcome.redux.WelcomeMiddleware +import com.tangem.tap.features.welcome.redux.WelcomeState import org.rekotlin.Middleware import org.rekotlin.StateType @@ -84,6 +85,7 @@ data class AppState( WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, ShopMiddleware().shopMiddleware, + WelcomeMiddleware().middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index 8a2f5c5ac1..97d4c27134 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -2,13 +2,15 @@ package com.tangem.tap.common.redux.navigation import android.content.Intent import com.tangem.tap.common.CustomTabsManager +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.openFragment import com.tangem.tap.common.extensions.popBackTo import com.tangem.tap.common.extensions.shareText import com.tangem.tap.common.redux.AppState +import com.tangem.tap.store import org.rekotlin.Middleware -val navigationMiddleware: Middleware = { dispatch, state -> +val navigationMiddleware: Middleware = { _, state -> { next -> { action -> if (action is NavigationAction) { @@ -16,16 +18,24 @@ val navigationMiddleware: Middleware = { dispatch, state -> when (action) { is NavigationAction.NavigateTo -> { navState?.activity?.get()?.openFragment( - action.screen, - action.addToBackstack, - action.fragmentShareTransition + screen = action.screen, + addToBackstack = action.addToBackstack, + fgShareTransition = action.fragmentShareTransition, ) } is NavigationAction.PopBackTo -> { - if (action.screen == AppScreen.Home) { - navState?.activity?.get()?.popBackTo(null, true) - } else { - navState?.activity?.get()?.popBackTo(action.screen) + when (val screen = action.screen) { + AppScreen.Home, + AppScreen.Welcome, + -> { + navState?.activity?.get()?.popBackTo(screen = null, inclusive = true) + if (navState?.backStack?.contains(screen) != true) { + store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + } + } + else -> { + navState?.activity?.get()?.popBackTo(screen = action.screen) + } } } is NavigationAction.OpenUrl -> { @@ -39,10 +49,11 @@ val navigationMiddleware: Middleware = { dispatch, state -> navState?.activity?.get()?.startActivity(intent) } is NavigationAction.Share -> { - navState?.activity?.get()?.let { - it.shareText(action.data) - } + navState?.activity?.get()?.shareText(action.data) } + is NavigationAction.ActivityCreated, + is NavigationAction.ActivityDestroyed, + -> Unit } } next(action) 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 e3d82a292a..cf60dc79f6 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 @@ -5,7 +5,7 @@ import org.rekotlin.StateType import java.lang.ref.WeakReference data class NavigationState( - val backStack: List = listOf(AppScreen.Home), + val backStack: List = emptyList(), val activity: WeakReference? = null, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 66b16d1cad..b1f24b354d 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -8,11 +8,13 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.CardFilter import com.tangem.common.CompletionResult import com.tangem.common.SuccessResponse +import com.tangem.common.UserCodeType import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.Config import com.tangem.common.core.TangemSdkError +import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.common.map @@ -46,7 +48,10 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co userTokensRepository: UserTokensRepository, additionalBlockchainsToDerive: Collection? = null, messageRes: Int? = null, + useBiometricsForAccessCode: Boolean = false, ): CompletionResult { + setAccessCodeRequestPolicy(useBiometricsForAccessCode) + val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive), @@ -175,6 +180,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return context.getString(stringResId, formatArgs) } + fun setAccessCodeRequestPolicy( + useBiometricsForAccessCode: Boolean, + ) { + tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) { + UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) + } else { + UserCodeRequestPolicy.Default + } + } + companion object { val config = Config( linkedTerminal = true, diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt new file mode 100644 index 0000000000..f649f4919f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.domain.model + +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.util.UserWalletId +import com.tangem.domain.common.util.userWalletId +import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl + +data class UserWallet( + val name: String, + val walletId: UserWalletId, + val artworkUrl: String, + val cardsInWallet: Set, + val scanResponse: ScanResponse, +) { + val cardId: String + get() = scanResponse.card.cardId + + companion object { + suspend operator fun invoke( + scanResponse: ScanResponse, + backupCardsIds: Set? = null, + ): UserWallet { + return with(scanResponse) { + UserWallet( + walletId = card.userWalletId, + name = productType.name, + artworkUrl = card.getOrLoadCardArtworkUrl(), + cardsInWallet = backupCardsIds?.plus(card.cardId) ?: setOf(card.cardId), + scanResponse = this, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt new file mode 100644 index 0000000000..954888733e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt @@ -0,0 +1,233 @@ +package com.tangem.tap.domain.scanCard + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.services.Result +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.extensions.withMainContext +import com.tangem.operations.backup.BackupService +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE +import com.tangem.tap.backupService +import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerType +import com.tangem.tap.features.disclaimer.redux.isAccepted +import com.tangem.tap.features.onboarding.OnboardingHelper +import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper +import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction +import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction +import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager +import com.tangem.tap.userTokensRepository +import com.tangem.wallet.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +// TODO: Create repository for that +object ScanCardProcessor { + suspend fun scan( + useBiometricsForAccessCode: Boolean = false, + additionalBlockchainsToDerive: Collection? = null, + onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, + onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, + onWalletNotCreated: suspend (() -> Unit) = {}, + onFailure: suspend (error: TangemError) -> Unit = {}, + onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {}, + ) = withMainContext { + onProgressStateChange(true) + onScanStateChange(true) + tangemSdkManager.scanProduct( + userTokensRepository = userTokensRepository, + additionalBlockchainsToDerive = additionalBlockchainsToDerive, + useBiometricsForAccessCode = useBiometricsForAccessCode, + ) + .doOnFailure { error -> + onProgressStateChange(false) + onScanStateChange(false) + onFailure(error) + } + .doOnSuccess { scanResponse -> + onScanStateChange(false) + checkForUnfinishedBackupForSaltPay( + backupService = backupService, + scanResponse = scanResponse, + onProgressStateChange = { onProgressStateChange(it) }, + nextHandler = { + showDisclaimerIfNeed( + scanResponse = scanResponse, + nextHandler = { + onScanSuccess( + scanResponse = scanResponse, + onProgressStateChange = onProgressStateChange, + onSuccess = onSuccess, + onWalletNotCreated = onWalletNotCreated, + onFailure = onFailure, + ) + }, + ) + }, + ) + + } + } + + /** + * It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards + * see BackupAction.CheckForUnfinishedBackup + * If user touches card other than Visa SaltPay - show dialog and block next processing + */ + private inline fun checkForUnfinishedBackupForSaltPay( + backupService: BackupService, + scanResponse: ScanResponse, + onProgressStateChange: (showProgress: Boolean) -> Unit, + nextHandler: (ScanResponse) -> Unit, + ) { + if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) { + nextHandler(scanResponse) + return + } + + val isTheSamePrimaryCard = backupService.primaryCardId + ?.let { it == scanResponse.card.cardId } + ?: false + + if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard) { + onProgressStateChange(false) + showSaltPayTapVisaLogoCardDialog() + } else { + nextHandler(scanResponse) + } + } + + private suspend inline fun showDisclaimerIfNeed( + scanResponse: ScanResponse, + crossinline nextHandler: suspend (ScanResponse) -> Unit, + ) { + val disclaimerType = DisclaimerType.get(scanResponse) + store.dispatch(DisclaimerAction.SetDisclaimerType(disclaimerType)) + + if (disclaimerType.isAccepted()) { + nextHandler((scanResponse)) + } else scope.launch(Dispatchers.Main) { + delay(DELAY_SDK_DIALOG_CLOSE) + store.dispatch( + DisclaimerAction.Show { + scope.launch { + nextHandler(scanResponse) + } + }, + ) + } + } + + private suspend inline fun onScanSuccess( + scanResponse: ScanResponse, + crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, + crossinline onWalletNotCreated: suspend () -> Unit, + crossinline onSuccess: suspend (ScanResponse) -> Unit, + crossinline onFailure: suspend (error: TangemError) -> Unit, + ) { + Analytics.send(IntroductionProcess.CardWasScanned()) + + val globalState = store.state.globalState + val tapWalletManager = globalState.tapWalletManager + tapWalletManager.updateConfigManager(scanResponse) + + Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId)) + + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) + + if (scanResponse.isSaltPay()) { + if (scanResponse.isSaltPayVisa()) { + val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse) + val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager) + delay(500) + withMainContext { + when (result) { + is Result.Success -> { + val isOnboardingCase = result.data + if (isOnboardingCase) { + onWalletNotCreated() + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false)) + store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config)) + store.dispatch(OnboardingSaltPayAction.Update) + navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) } + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) + onProgressStateChange(false) + } + } + is Result.Failure -> { + SaltPayExceptionHandler.handle(result.error) + delay(DELAY_SDK_DIALOG_CLOSE) + onFailure(TangemSdkError.ExceptionError(result.error)) + onProgressStateChange(false) + } + } + } + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + if (scanResponse.card.backupStatus?.isActive == false) { + showSaltPayTapVisaLogoCardDialog() + } else { + onSuccess(scanResponse) + } + onProgressStateChange(false) + } + } else { + if (OnboardingHelper.isOnboardingCase(scanResponse)) { + onWalletNotCreated() + store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true)) + val appScreen = OnboardingHelper.whereToNavigate(scanResponse) + navigateTo(appScreen) { onProgressStateChange(it) } + } else { + if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) { + onWalletNotCreated() + store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly)) + navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) } + } else { + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) + onProgressStateChange(false) + } + } + } + } + + private fun showSaltPayTapVisaLogoCardDialog() { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.saltpay_error_empty_backup_title, + messageId = R.string.saltpay_error_empty_backup_message, + ), + ) + } + + private suspend inline fun navigateTo( + screen: AppScreen, + onProgressStateChange: (showProgress: Boolean) -> Unit, + ) { + delay(DELAY_SDK_DIALOG_CLOSE) + store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + onProgressStateChange(false) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt new file mode 100644 index 0000000000..05efbdf35f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.domain.userWalletList + +import com.tangem.common.CompletionResult +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import kotlinx.coroutines.flow.Flow + +interface UserWalletsListManager { + val userWallets: Flow> + val selectedUserWallet: Flow + val selectedUserWalletSync: UserWallet? + val isLocked: Flow + val isLockedSync: Boolean + val hasSavedUserWallets: Boolean + + suspend fun unlockWithBiometry(): CompletionResult + suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult + fun lock() + + suspend fun selectWallet(walletId: UserWalletId): CompletionResult + + suspend fun save(userWallet: UserWallet): CompletionResult + suspend fun update(userWallet: UserWallet): CompletionResult + + suspend fun delete(walletIds: List): CompletionResult + suspend fun clear(): CompletionResult + + companion object +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt new file mode 100644 index 0000000000..f60c7f3de0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.userWalletList.di + +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import com.tangem.tap.domain.userWalletList.implementation.DummyUserWalletsListManager + +fun UserWalletsListManager.Companion.provideDummyImplementation(): UserWalletsListManager { + return DummyUserWalletsListManager() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt new file mode 100644 index 0000000000..bcc6c05b7f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.domain.userWalletList.implementation + +import com.tangem.common.CompletionResult +import com.tangem.common.catching +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.userWalletList.UserWalletsListManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +class DummyUserWalletsListManager : UserWalletsListManager { + override val userWallets: Flow> + get() = flowOf(emptyList()) + override val selectedUserWallet: Flow + get() = flowOf() + override val selectedUserWalletSync: UserWallet? + get() = null + override val isLocked: Flow + get() = flowOf(true) + override val isLockedSync: Boolean + get() = true + override val hasSavedUserWallets: Boolean + get() = false + + override suspend fun unlockWithBiometry(): CompletionResult { + return CompletionResult.Success(null) + } + + override suspend fun unlockWithCard(userWallet: UserWallet): CompletionResult { + return CompletionResult.Success(Unit) + } + + override fun lock() { + /* no-op */ + } + + override suspend fun selectWallet(walletId: UserWalletId): CompletionResult { + return catching { + error("Not implemented") + } + } + + override suspend fun save(userWallet: UserWallet): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun update(userWallet: UserWallet): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun delete(walletIds: List): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun clear(): CompletionResult { + return CompletionResult.Success(Unit) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt new file mode 100644 index 0000000000..eb7fca3d46 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -0,0 +1,90 @@ +package com.tangem.tap.features.welcome.redux + +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.scanCard.ScanCardProcessor +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager +import kotlinx.coroutines.launch +import org.rekotlin.Middleware + +internal class WelcomeMiddleware { + val middleware: Middleware = { _, _ -> + { next -> + { action -> + if (action is WelcomeAction) { + handleAction(action) + } + next(action) + } + } + } + + private fun handleAction(action: WelcomeAction) { + when (action) { + is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometry() + is WelcomeAction.ProceedWithCard -> proceedWithCard() + is WelcomeAction.ProceedWithBiometrics.Error -> Unit + is WelcomeAction.ProceedWithCard.Error -> Unit + is WelcomeAction.ProceedWithBiometrics.Success -> Unit + is WelcomeAction.ProceedWithCard.Success -> Unit + is WelcomeAction.CloseError -> Unit + } + } + + private fun proceedWithBiometry() { + scope.launch { + userWalletsListManager.unlockWithBiometry() + .doOnFailure { error -> + store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error)) + } + .doOnSuccess { selectedUserWallet -> + if (selectedUserWallet != null) { + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) + store.onUserWalletSelected(selectedUserWallet) + } + } + } + } + + private fun proceedWithCard() = scope.launch { + scanCardInternal { scanResponse -> + val userWallet = UserWallet(scanResponse) + + userWalletsListManager.unlockWithCard(userWallet) + .doOnFailure { error -> + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) + } + .doOnSuccess { + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) + store.onUserWalletSelected(userWallet) + } + } + } + + private suspend inline fun scanCardInternal( + crossinline onCardScanned: suspend (ScanResponse) -> Unit, + ) { + ScanCardProcessor.scan( + onSuccess = { scanResponse -> + scope.launch { onCardScanned(scanResponse) } + }, + onFailure = { + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(it)) + }, + onWalletNotCreated = { + store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt new file mode 100644 index 0000000000..d5f3e0276b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.welcome.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +internal object WelcomeReducer { + fun reduce(action: Action, state: AppState): WelcomeState { + return if (action is WelcomeAction) { + internalReduce(action, state.welcomeState) + } else state.welcomeState + } + + private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState { + return when (action) { + is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true) + is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true) + is WelcomeAction.ProceedWithBiometrics.Error -> state.copy( + error = action.error, + isUnlockWithBiometricsInProgress = false, + ) + is WelcomeAction.ProceedWithCard.Error -> state.copy( + error = action.error, + isUnlockWithCardInProgress = false, + ) + is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false) + is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false) + is WelcomeAction.CloseError -> state.copy(error = null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt index 2d89c4d902..a4fce23f93 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt @@ -1,5 +1,10 @@ package com.tangem.tap.features.welcome.redux +import com.tangem.common.core.TangemError import org.rekotlin.StateType -class WelcomeState : StateType \ No newline at end of file +data class WelcomeState( + val isUnlockWithBiometricsInProgress: Boolean = false, + val isUnlockWithCardInProgress: Boolean = false, + val error: TangemError? = null, +) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index b80c8ac31b..07c9af833c 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -20,6 +20,7 @@ import androidx.fragment.app.viewModels import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.fragments.ComposeFragment import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent import com.tangem.wallet.R @@ -37,7 +38,7 @@ internal class WelcomeFragment : ComposeFragment() { state: WelcomeScreenState, ) { val snackbarHostState = remember { SnackbarHostState() } - val errorMessage by rememberUpdatedState(newValue = state.error?.customMessage) + val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val backgroundColor = colorResource(id = R.color.background_primary) SystemBarsEffect { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index 7a741a391f..d0d883148a 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -1,11 +1,11 @@ package com.tangem.tap.features.welcome.ui import androidx.compose.runtime.Immutable -import com.tangem.common.core.TangemError +import com.tangem.tap.features.details.ui.cardsettings.TextReference @Immutable internal data class WelcomeScreenState( val showUnlockWithBiometricsProgress: Boolean = false, val showUnlockWithCardProgress: Boolean = false, - val error: TangemError? = null, + val error: TextReference? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index ab9e8e6673..a047d8518e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -1,6 +1,8 @@ package com.tangem.tap.features.welcome.ui import androidx.lifecycle.ViewModel +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.store @@ -15,6 +17,7 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { init { subscribeToStoreChanges() + initGlobalState() } fun unlockWallets() { @@ -31,8 +34,16 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { override fun newState(state: WelcomeState) { stateInternal.update { prevState -> - // TODO: Update screen state - prevState + prevState.copy( + showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress, + showUnlockWithCardProgress = state.isUnlockWithCardInProgress, + error = state.error + ?.takeUnless { it.silent } + ?.let { error -> + error.messageResId?.let { TextReference.Res(it) } + ?: TextReference.Str(error.customMessage) + }, + ) } } @@ -46,4 +57,10 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { .select { it.welcomeState } } } + + private fun initGlobalState() { + store.dispatch(GlobalAction.RestoreAppCurrency) + store.dispatch(GlobalAction.ExchangeManager.Init) + store.dispatch(GlobalAction.FetchUserCountry) + } } \ No newline at end of file From 41dea25009771d79c85aaa408509760d5c8e17c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Nov 2022 18:26:31 +0300 Subject: [PATCH 07/16] Updated on 2026-08-14 --- .../com/tangem/tap/ActivityResultCaller.kt | 8 ++ .../tangem/tap/ForegroundActivityObserver.kt | 32 ++++- .../java/com/tangem/tap/TapApplication.kt | 2 + .../com/tangem/tap/common/redux/AppReducer.kt | 2 + .../com/tangem/tap/common/redux/AppState.kt | 2 + .../redux/navigation/NavigationAction.kt | 2 + .../redux/navigation/NavigationMiddleware.kt | 26 ++++ .../com/tangem/tap/domain/TangemSdkManager.kt | 42 ++++-- .../com/tangem/tap/domain/model/UserWallet.kt | 3 + .../saveWallet/redux/SaveWalletAction.kt | 11 ++ .../saveWallet/redux/SaveWalletMiddleware.kt | 124 ++++++++++++++++++ .../saveWallet/redux/SaveWalletReducer.kt | 51 +++++++ .../saveWallet/redux/SaveWalletState.kt | 15 ++- .../ui/SaveWalletBottomSheetFragment.kt | 3 +- .../saveWallet/ui/SaveWalletScreenState.kt | 4 +- .../saveWallet/ui/SaveWalletViewModel.kt | 23 +++- .../tap/persistence/PreferencesStorage.kt | 21 +++ 17 files changed, 348 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/ActivityResultCaller.kt create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt diff --git a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt new file mode 100644 index 0000000000..16a05117ac --- /dev/null +++ b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt @@ -0,0 +1,8 @@ +package com.tangem.tap + +import android.content.Intent +import androidx.activity.result.ActivityResultLauncher + +interface ActivityResultCaller { + val activityResultLauncher: ActivityResultLauncher? +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt index 806769df5f..9664f38966 100644 --- a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -2,11 +2,18 @@ package com.tangem.tap import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks +import android.content.Intent import android.os.Bundle -import java.util.WeakHashMap +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import java.util.* import kotlin.reflect.KClass -class ForegroundActivityObserver { +class ForegroundActivityObserver : ActivityResultCaller { + override var activityResultLauncher: ActivityResultLauncher? = null + private set + private val activities = WeakHashMap, Activity>() val foregroundActivity: Activity? @@ -15,25 +22,38 @@ class ForegroundActivityObserver { .firstOrNull() ?.value - val callbacks get() = Callbacks() + internal val callbacks: ActivityLifecycleCallbacks + get() = Callbacks() + + internal inner class Callbacks : ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + activityResultLauncher = (activity as? AppCompatActivity)?.registerForActivityResult( + ActivityResultContracts.StartActivityForResult(), + ) { + /* no-op */ + } + } - inner class Callbacks : ActivityLifecycleCallbacks { override fun onActivityResumed(activity: Activity) { activities[activity::class] = activity } override fun onActivityDestroyed(activity: Activity) { activities.remove(activity::class) + if (activities.isEmpty()) { + activityResultLauncher = null + } } - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - } override fun onActivityStarted(activity: Activity) { } + override fun onActivityPaused(activity: Activity) { } + override fun onActivityStopped(activity: Activity) { } + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { } } diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 287f2d7a1e..4b801de36a 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -53,6 +53,7 @@ import javax.inject.Inject lateinit var store: Store lateinit var foregroundActivityObserver: ForegroundActivityObserver +lateinit var activityResultCaller: ActivityResultCaller lateinit var preferencesStorage: PreferencesStorage lateinit var walletConnectRepository: WalletConnectRepository lateinit var shopService: TangemShopService @@ -82,6 +83,7 @@ class TapApplication : Application(), ImageLoaderFactory { } foregroundActivityObserver = ForegroundActivityObserver() + activityResultCaller = foregroundActivityObserver registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) initMoshiConverter() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 6e55404733..6d2bf6af62 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -10,6 +10,7 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteRedu import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer +import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer import com.tangem.tap.features.send.redux.reducers.SendScreenReducer import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.tokens.redux.TokensReducer @@ -38,6 +39,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), shopState = ShopReducer.reduce(action, state.shopState), welcomeState = WelcomeReducer.reduce(action, state), + saveWalletState = SaveWalletReducer.reduce(action, state), ) } 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 b8b4bf689a..d85b69df46 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.SaveWalletMiddleware 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 @@ -86,6 +87,7 @@ data class AppState( BackupMiddleware().backupMiddleware, ShopMiddleware().shopMiddleware, WelcomeMiddleware().middleware, + SaveWalletMiddleware().middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt index e397d78d8a..df26b85edc 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt @@ -18,6 +18,8 @@ sealed class NavigationAction : Action { data class OpenDocument(val url: Uri) : NavigationAction() + object OpenBiometricsSettings : NavigationAction() + data class Share(val data: String) : NavigationAction() data class ActivityCreated(val activity: WeakReference) : NavigationAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index 97d4c27134..da9430b1e1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -1,6 +1,10 @@ package com.tangem.tap.common.redux.navigation import android.content.Intent +import android.hardware.biometrics.BiometricManager +import android.os.Build +import android.provider.Settings +import com.tangem.tap.activityResultCaller import com.tangem.tap.common.CustomTabsManager import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.openFragment @@ -48,6 +52,28 @@ val navigationMiddleware: Middleware = { _, state -> intent.data = action.url navState?.activity?.get()?.startActivity(intent) } + is NavigationAction.OpenBiometricsSettings -> { + val settingsAction = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + Settings.ACTION_BIOMETRIC_ENROLL + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.P -> { + Settings.ACTION_FINGERPRINT_ENROLL + } + else -> { + Settings.ACTION_SETTINGS + } + } + val intent = Intent(settingsAction).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BiometricManager.Authenticators.BIOMETRIC_STRONG, + ) + } + } + activityResultCaller.activityResultLauncher?.launch(intent) + } is NavigationAction.Share -> { navState?.activity?.get()?.shareText(action.data) } diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index b1f24b354d..b7a54c7d6d 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -5,19 +5,12 @@ import androidx.annotation.StringRes import com.tangem.Message import com.tangem.TangemSdk import com.tangem.blockchain.common.Blockchain -import com.tangem.common.CardFilter -import com.tangem.common.CompletionResult -import com.tangem.common.SuccessResponse -import com.tangem.common.UserCodeType +import com.tangem.common.* import com.tangem.common.card.FirmwareVersion -import com.tangem.common.core.CardIdDisplayFormat -import com.tangem.common.core.CardSessionRunnable -import com.tangem.common.core.Config -import com.tangem.common.core.TangemSdkError -import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.core.* import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.hdWallet.DerivationPath -import com.tangem.common.map +import com.tangem.common.usersCode.UserCodeRepository import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse import com.tangem.operations.CommandResponse @@ -43,6 +36,11 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) { + val canUseBiometry: Boolean + get() = tangemSdk.biometricManager.canAuthenticate || canEnrollBiometrics + + val canEnrollBiometrics: Boolean + get() = tangemSdk.biometricManager.canEnrollBiometrics suspend fun scanProduct( userTokensRepository: UserTokensRepository, @@ -104,6 +102,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co .map { CardDTO(it) } } + suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { + return createUserCodeRepository().save( + cardIds = cardsIds, + userCode = UserCode( + type = UserCodeType.AccessCode, + stringValue = accessCode, + ), + ) + } + suspend fun setPasscode(cardId: String?): CompletionResult { return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), @@ -136,9 +144,14 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co ) } - suspend fun scanCard(): CompletionResult { + suspend fun scanCard( + cardId: String? = null, + useBiometricsForAccessCode: Boolean = false, + ): CompletionResult { + setAccessCodeRequestPolicy(useBiometricsForAccessCode) return runTaskAsyncReturnOnMain( runnable = ScanTask(), + cardId = cardId, initialMessage = Message(context.getString(R.string.initial_message_tap_header)), ) .map { CardDTO(it) } @@ -190,6 +203,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } } + private fun createUserCodeRepository() = with(tangemSdk) { + UserCodeRepository( + biometricManager = biometricManager, + secureStorage = secureStorage, + ) + } + companion object { val config = Config( linkedTerminal = true, diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt index f649f4919f..6ce60fae85 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt @@ -15,6 +15,9 @@ data class UserWallet( val cardId: String get() = scanResponse.card.cardId + val hasAccessCode: Boolean + get() = scanResponse.card.isAccessCodeSet + companion object { suspend operator fun invoke( scanResponse: ScanResponse, 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 index 3e0bbe4a7e..46581d4432 100644 --- 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 @@ -1,9 +1,16 @@ package com.tangem.tap.features.saveWallet.redux import com.tangem.common.core.TangemError +import com.tangem.domain.common.ScanResponse import org.rekotlin.Action internal sealed interface SaveWalletAction : Action { + data class ProvideAdditionalInfo( + val scanResponse: ScanResponse, + val accessCode: String?, + val backupCardsIds: Set?, + ) : SaveWalletAction + object Save : SaveWalletAction { object Success : SaveWalletAction data class Error(val error: TangemError) : SaveWalletAction @@ -12,6 +19,10 @@ internal sealed interface SaveWalletAction : Action { object Dismiss : SaveWalletAction object CloseError : SaveWalletAction + object EnrollBiometrics : SaveWalletAction { + object Enroll : SaveWalletAction + object Cancel : SaveWalletAction + } object SaveWalletWasShown : SaveWalletAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt new file mode 100644 index 0000000000..844b55b115 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -0,0 +1,124 @@ +package com.tangem.tap.features.saveWallet.redux + +import com.tangem.common.CompletionResult +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.flatMap +import com.tangem.common.map +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager +import com.tangem.tap.userWalletsListManager +import kotlinx.coroutines.launch +import org.rekotlin.Middleware + +internal class SaveWalletMiddleware { + val middleware: Middleware = { _, stateProvider -> + { next -> + { action -> + val state = stateProvider() + if (action is SaveWalletAction && state != null) { + handleAction(action, state.saveWalletState) + } + next(action) + } + } + } + + private fun handleAction(action: SaveWalletAction, state: SaveWalletState) { + when (action) { + is SaveWalletAction.Save -> saveWalletIfBiometricsEnrolled(state) + is SaveWalletAction.Save.Success -> popBack() + is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics() + is SaveWalletAction.SaveWalletWasShown -> saveWalletWasShown() + is SaveWalletAction.ProvideAdditionalInfo, + is SaveWalletAction.Dismiss, + is SaveWalletAction.CloseError, + is SaveWalletAction.Save.Error, + is SaveWalletAction.EnrollBiometrics, + is SaveWalletAction.EnrollBiometrics.Cancel, + -> Unit + } + } + + private fun enrollBiometrics() { + store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) + } + + private fun saveWalletIfBiometricsEnrolled(state: SaveWalletState) { + if (tangemSdkManager.canEnrollBiometrics) { + store.dispatchOnMain(SaveWalletAction.EnrollBiometrics) + } else { + saveWallet(state) + } + } + + private fun saveWallet(state: SaveWalletState) { + val scanResponse = state.additionalInfo?.scanResponse + ?: store.state.globalState.scanResponse + ?: return + + scope.launch { + val userWallet = UserWallet( + scanResponse = scanResponse, + backupCardsIds = state.additionalInfo?.backupCardsIds, + ) + + userWalletsListManager.save(userWallet) + .flatMap { + trySaveAccessCode( + userWallet = userWallet, + additionalInfo = state.additionalInfo, + ) + } + .doOnFailure { error -> + store.dispatchOnMain(SaveWalletAction.Save.Error(error)) + } + .doOnSuccess { + preferencesStorage.shouldSaveUserWallets = true + preferencesStorage.shouldSaveAccessCodes = true + store.dispatchOnMain(SaveWalletAction.Save.Success) + store.onUserWalletSelected(userWallet) + } + } + } + + private fun popBack() { + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + + private fun saveWalletWasShown() { + preferencesStorage.shouldShowSaveWallet = false + } + + private suspend fun trySaveAccessCode( + userWallet: UserWallet, + additionalInfo: SaveWalletState.WalletAdditionalInfo?, + ): CompletionResult { + return when { + additionalInfo?.accessCode != null -> { + tangemSdkManager.saveAccessCode( + accessCode = additionalInfo.accessCode, + cardsIds = userWallet.cardsInWallet, + ) + } + userWallet.hasAccessCode -> { + tangemSdkManager + .scanCard( + cardId = userWallet.cardId, + useBiometricsForAccessCode = true, + ) + .map { /* no-op */ } + } + else -> { + CompletionResult.Success(Unit) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt new file mode 100644 index 0000000000..9907306ea6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.features.saveWallet.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +internal object SaveWalletReducer { + fun reduce(action: Action, state: AppState): SaveWalletState { + return if (action is SaveWalletAction) { + internalReduce(action, state.saveWalletState) + } else state.saveWalletState + } + + private fun internalReduce(action: SaveWalletAction, state: SaveWalletState): SaveWalletState { + return when (action) { + is SaveWalletAction.ProvideAdditionalInfo -> state.copy( + additionalInfo = SaveWalletState.WalletAdditionalInfo( + scanResponse = action.scanResponse, + accessCode = action.accessCode, + backupCardsIds = action.backupCardsIds, + ), + ) + is SaveWalletAction.Save -> state.copy( + isSaveInProgress = true, + ) + is SaveWalletAction.Save.Error -> state.copy( + error = action.error, + isSaveInProgress = false, + ) + is SaveWalletAction.Save.Success -> state.copy( + additionalInfo = null, + isSaveInProgress = false, + ) + is SaveWalletAction.Dismiss -> state.copy( + additionalInfo = null, + ) + is SaveWalletAction.CloseError -> state.copy( + error = null, + ) + is SaveWalletAction.EnrollBiometrics -> state.copy( + needEnrollBiometrics = true, + ) + is SaveWalletAction.EnrollBiometrics.Enroll, + is SaveWalletAction.EnrollBiometrics.Cancel, + -> state.copy( + needEnrollBiometrics = false, + isSaveInProgress = false, + ) + is SaveWalletAction.SaveWalletWasShown -> state + } + } +} \ 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 index d43ee7c96b..50cb237ff2 100644 --- 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 @@ -1,5 +1,18 @@ package com.tangem.tap.features.saveWallet.redux +import com.tangem.common.core.TangemError +import com.tangem.domain.common.ScanResponse import org.rekotlin.StateType -class SaveWalletState : StateType \ No newline at end of file +data class SaveWalletState( + val additionalInfo: WalletAdditionalInfo? = null, + val isSaveInProgress: Boolean = false, + val needEnrollBiometrics: Boolean = false, + val error: TangemError? = null, +) : StateType { + data class WalletAdditionalInfo( + val scanResponse: ScanResponse, + val accessCode: String?, + val backupCardsIds: Set?, + ) +} \ 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 index 6abc3c607e..65da3e8dc5 100644 --- 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 @@ -20,6 +20,7 @@ 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.details.ui.cardsettings.resolveReference import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { @@ -43,7 +44,7 @@ internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment - // TODO: Update screen state - prevState + prevState.copy( + showProgress = state.isSaveInProgress, + enrollBiometricsDialog = if (state.needEnrollBiometrics) createEnrollBiometricsDialog() else null, + error = state.error + ?.takeUnless { it.silent } + ?.let { error -> + error.messageResId?.let { TextReference.Res(it) } + ?: TextReference.Str(error.customMessage) + }, + ) } } @@ -48,4 +58,13 @@ internal class SaveWalletViewModel : ViewModel(), StoreSubscriber Date: Thu, 1 Dec 2022 14:54:01 +0300 Subject: [PATCH 08/16] Updated on 2026-08-14 --- .../ui/WalletSelectorBottomSheetFragment.kt | 17 +-- .../ui/WalletSelectorScreenState.kt | 10 +- .../ui/WalletSelectorViewModel.kt | 3 +- .../walletSelector/ui/components/MockData.kt | 30 +++-- .../WallectSelectorScreenContent.kt | 122 ++++++++++++------ .../ui/components/WalletList.kt | 66 ---------- .../walletSelector/ui/model/UserWalletItem.kt | 62 +++++---- 7 files changed, 148 insertions(+), 162 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index 47c6ca9bf2..3637f2ea5e 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -5,15 +5,12 @@ 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.runtime.* import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.fragment.app.viewModels import com.tangem.core.ui.fragments.ComposeBottomSheetFragment import com.tangem.core.ui.res.TangemTheme @@ -29,6 +26,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment = listOf(), + val multiCurrencyWallets: List = emptyList(), + val singleCurrencyWallets: List = emptyList(), val selectedWalletId: String? = null, - val fiatCurrency: FiatCurrency = FiatCurrency.Default, - val isLocked: Boolean = true, + val isLocked: Boolean = false, val editingWalletsIds: List = listOf(), val renameWalletDialog: RenameWalletDialog? = null, val showAddCardProgress: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt index cb6d75b0b0..fcb8f32c7e 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt @@ -60,7 +60,8 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber, + singleCurrencyWallets: List, + selectedWalletId: String?, + checkedWalletIds: List, + isLocked: Boolean, + onWalletClick: (walletId: String) -> Unit, + onWalletLongClick: (walletId: String) -> Unit, +) { + Column(modifier = modifier) { + val walletsSection = @Composable { wallets: List -> + wallets.forEachIndexed { index, wallet -> + if (index == 0) { + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), + text = wallet.headerText.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } + WalletItem( + modifier = Modifier + .combinedClickable( + onClick = { onWalletClick(wallet.id) }, + onLongClick = { onWalletLongClick(wallet.id) }, + ) + .padding(all = TangemTheme.dimens.spacing16), + wallet = wallet, + isSelected = wallet.id == selectedWalletId, + isChecked = wallet.id in checkedWalletIds, + isLocked = isLocked && wallet.id != selectedWalletId, + ) + } + } + + walletsSection(multiCurrencyWallets) + walletsSection(singleCurrencyWallets) + } +} + // region Preview @Composable private fun WalletSelectorScreenContentSample( @@ -221,7 +263,7 @@ private fun WalletSelectorScreenContent_EditWallets_Sample( shape = TangemTheme.shapes.bottomSheet, ), state = MockData.state - .copy(editingWalletsIds = listOf(MockData.userWalletList[2].id)), + .copy(editingWalletsIds = listOf(MockData.state.multiCurrencyWallets[2].id)), onWalletClick = { /* no-op */ }, onWalletLongClick = { /* no-op */ }, onUnlockClick = { /* no-op */ }, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt deleted file mode 100644 index ab1f4f9c40..0000000000 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletList.kt +++ /dev/null @@ -1,66 +0,0 @@ -@file:OptIn(ExperimentalFoundationApi::class) - -package com.tangem.tap.features.walletSelector.ui.components - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.ui.cardsettings.resolveReference -import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem - -@Composable -internal fun WalletList( - modifier: Modifier = Modifier, - wallets: List, - selectedWalletId: String?, - checkedWalletIds: List, - isLocked: Boolean, - onWalletClick: (walletId: String) -> Unit, - onWalletLongClick: (walletId: String) -> Unit, -) { - val multiCurrencyWallets = remember(wallets) { - wallets.filter { it.type is UserWalletItem.Type.MultiCurrency } - } - val singleCurrencyWallets = remember(wallets) { - wallets.filter { it.type is UserWalletItem.Type.SingleCurrency } - } - LazyColumn(modifier = modifier) { - val walletSection: (List) -> Unit = { wallets -> - itemsIndexed( - items = wallets, - key = { _, item -> item.id }, - ) { index, wallet -> - if (index == 0) { - Text( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), - text = wallet.type.headerText.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - } - WalletItem( - modifier = Modifier - .combinedClickable( - onClick = { onWalletClick(wallet.id) }, - onLongClick = { onWalletLongClick(wallet.id) }, - ) - .padding(all = TangemTheme.dimens.spacing16), - wallet = wallet, - isSelected = wallet.id == selectedWalletId, - isChecked = wallet.id in checkedWalletIds, - isLocked = isLocked && wallet.id != selectedWalletId, - ) - } - } - - walletSection(multiCurrencyWallets) - walletSection(singleCurrencyWallets) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt index 41f0ace5cd..cadbde80db 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/UserWalletItem.kt @@ -5,42 +5,38 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.wallet.R @Immutable -internal data class UserWalletItem( - val id: String, - val name: String, - val imageUrl: String, - val balance: Balance, - val type: Type, -) { +internal sealed interface UserWalletItem { + val id: String + val name: String + val imageUrl: String + val balance: Balance + + val headerText: TextReference + get() = when (this) { + is MultiCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_multi_header) + is SingleCurrencyUserWalletItem -> TextReference.Res(R.string.user_wallet_list_single_header) + } + @Immutable data class Balance( val amount: String, val isLoading: Boolean, - ) { - companion object { - val Loading = Balance( - amount = "0.00", - isLoading = true, - ) - } - } + ) +} - sealed interface Type { - val headerText: TextReference +internal data class MultiCurrencyUserWalletItem( + override val id: String, + override val name: String, + override val imageUrl: String, + override val balance: UserWalletItem.Balance, + val tokensCount: Int, + val cardsInWallet: Int, +) : UserWalletItem - @Immutable - class MultiCurrency( - val tokensCount: Int, - val cardsInWallet: Int, - ) : Type { - override val headerText: TextReference - get() = TextReference.Res(R.string.user_wallet_list_multi_header) - } - - @Immutable - class SingleCurrency(val tokenName: String) : Type { - override val headerText: TextReference - get() = TextReference.Res(R.string.user_wallet_list_single_header) - } - } -} \ No newline at end of file +internal data class SingleCurrencyUserWalletItem( + override val id: String, + override val name: String, + override val imageUrl: String, + override val balance: UserWalletItem.Balance, + val tokenName: String, +) : UserWalletItem \ No newline at end of file From b1a901d127b739ccaf965d90a5b0becb53bfef13 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Dec 2022 14:55:03 +0300 Subject: [PATCH 09/16] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../ui/components/WalletItem.kt | 404 +++++++++++------- .../com/tangem/core/ui/res/TangemDimens.kt | 2 +- 3 files changed, 260 insertions(+), 147 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d1e7b40573..37bc6d5ed2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -192,6 +192,7 @@ dependencies { implementation(Library.viewBindingDelegate) implementation(Library.armadillo) implementation(Library.googlePlayServicesWallet) + implementation(Library.composeShimmer) /** Testing libraries */ testImplementation(Test.junit) diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt index 6837d47a7f..f33664bbfb 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -1,18 +1,8 @@ -@file:OptIn(ExperimentalComposeUiApi::class) - package com.tangem.tap.features.walletSelector.ui.components import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.Text @@ -34,8 +24,11 @@ import com.tangem.core.ui.components.SpacerW6 import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.compose.TangemTypography +import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem +import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem import com.tangem.wallet.R +import com.valentinilk.shimmer.shimmer @Composable internal fun WalletItem( @@ -48,157 +41,276 @@ internal fun WalletItem( Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, + ) { + WalletCardImage( + cardImageUrl = wallet.imageUrl, + isChecked = isChecked, + isSelected = isSelected, + ) + SpacerW8() + WalletInfo( + modifier = Modifier.weight(weight = .6f), + wallet = wallet, + isSelected = isSelected, + ) + SpacerW6() + TokensInfo( + modifier = Modifier.weight(weight = .4f), + isLocked = isLocked, + balance = wallet.balance, + tokensCount = (wallet as? MultiCurrencyUserWalletItem)?.tokensCount, + ) + } +} + +@Composable +private fun WalletCardImage( + modifier: Modifier = Modifier, + cardImageUrl: String, + isChecked: Boolean, + isSelected: Boolean, +) { + Box( + modifier = modifier + .width(TangemTheme.dimens.size62) + .height(TangemTheme.dimens.size42), + ) { + val cardImageModifier = Modifier + .align(Alignment.Center) + .width(TangemTheme.dimens.size56) + .height(TangemTheme.dimens.size32) + .clip(TangemTheme.shapes.roundedCornersSmall) + + SubcomposeAsyncImage( + modifier = cardImageModifier, + model = ImageRequest.Builder(LocalContext.current) + .data(cardImageUrl) + .crossfade(true) + .build(), + loading = { CardImageShimmer(modifier = cardImageModifier) }, + error = { CardImagePlaceholder(modifier = cardImageModifier) }, + contentDescription = null, + ) + + when { + isChecked -> { + Box( + modifier = cardImageModifier + .background( + color = TangemTheme.colors.icon.accent.copy(alpha = .5f), + ), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.primary2, + contentDescription = null, + ) + } + } + isSelected -> { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size18) + .background( + color = Color.White, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier + .padding(all = (0.5).dp), + painter = painterResource(id = R.drawable.ic_check_circle_18), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } + else -> Unit + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun WalletInfo( + modifier: Modifier = Modifier, + wallet: UserWalletItem, + isSelected: Boolean, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.SpaceAround, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = wallet.name, + color = if (isSelected) TangemTheme.colors.text.accent else TangemTheme.colors.text.primary1, + style = TangemTypography.subtitle1, + ) + SpacerH2() + Text( + modifier = Modifier.fillMaxWidth(), + text = when (wallet) { + is MultiCurrencyUserWalletItem -> pluralStringResource( + id = R.plurals.card_label_card_count, + count = wallet.cardsInWallet, + wallet.cardsInWallet, + ) + is SingleCurrencyUserWalletItem -> wallet.tokenName + }, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun TokensInfo( + modifier: Modifier = Modifier, + isLocked: Boolean, + balance: UserWalletItem.Balance, + tokensCount: Int?, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.SpaceAround, + horizontalAlignment = Alignment.End, + ) { + if (isLocked) { + LockedPlaceholder() + } else { + if (balance.isLoading) { + LoadingTokensInfo( + isMultiCurrencyWallet = tokensCount != null, + ) + } else { + LoadedTokensInfo( + balanceAmount = balance.amount, + tokensCount = tokensCount, + ) + } + } + } +} + +@Composable +private fun LoadingTokensInfo( + modifier: Modifier = Modifier, + isMultiCurrencyWallet: Boolean, +) { + Column( + modifier = modifier.shimmer(), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceAround, ) { Box( modifier = Modifier .width(TangemTheme.dimens.size62) - .height(TangemTheme.dimens.size42), - ) { - val cardImageModifier = Modifier - .align(Alignment.Center) - .width(TangemTheme.dimens.size56) - .height(TangemTheme.dimens.size32) - .clip(TangemTheme.shapes.roundedCornersSmall) - SubcomposeAsyncImage( - modifier = cardImageModifier, - model = ImageRequest.Builder(LocalContext.current) - .data(wallet.imageUrl) - .crossfade(true) - .build(), - loading = { CardImagePlaceholder(modifier = cardImageModifier) }, - error = { CardImagePlaceholder(modifier = cardImageModifier) }, - contentDescription = null, - ) - when { - isChecked -> { - Box( - modifier = cardImageModifier - .background( - color = TangemTheme.colors.icon.accent.copy(alpha = .5f), - ), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - tint = TangemTheme.colors.icon.primary2, - contentDescription = null, - ) - } - } - isSelected -> { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .background( - color = Color.White, - shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .padding(all = (0.5).dp), - painter = painterResource(id = R.drawable.ic_check_circle_18), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } - } - else -> Unit - } - } - SpacerW8() - Column( - modifier = Modifier.weight(weight = .6f), - verticalArrangement = Arrangement.SpaceAround, - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = wallet.name, - color = if (isSelected) TangemTheme.colors.text.accent else TangemTheme.colors.text.primary1, - style = TangemTypography.subtitle1, - ) + .height(TangemTheme.dimens.size16) + .background( + color = TangemTheme.colors.button.secondary, + shape = TangemTheme.shapes.roundedCornersSmall2, + ), + ) + if (isMultiCurrencyWallet) { SpacerH2() - Text( - modifier = Modifier.fillMaxWidth(), - text = when (val type = wallet.type) { - is UserWalletItem.Type.MultiCurrency -> pluralStringResource( - id = R.plurals.card_label_card_count, - count = type.cardsInWallet, - type.cardsInWallet, - ) - - is UserWalletItem.Type.SingleCurrency -> type.tokenName - }, - style = TangemTheme.typography.caption, - color = TangemTheme.colors.text.tertiary, - ) - } - SpacerW6() - if (isLocked) { Box( modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing2) + .width(TangemTheme.dimens.size48) + .height(TangemTheme.dimens.size12) .background( color = TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, + shape = TangemTheme.shapes.roundedCornersSmall2, ), - ) { - Icon( - modifier = Modifier - .padding( - vertical = TangemTheme.dimens.spacing8, - horizontal = TangemTheme.dimens.spacing12, - ) - .size(TangemTheme.dimens.size20), - painter = painterResource(id = R.drawable.ic_locked_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } - } else { - Column( - modifier = Modifier.weight(weight = .4f), - verticalArrangement = Arrangement.SpaceAround, - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = wallet.balance.amount, - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.End, - ) - when (val type = wallet.type) { - is UserWalletItem.Type.MultiCurrency -> { - SpacerH2() - Text( - modifier = Modifier.fillMaxWidth(), - text = pluralStringResource( - id = R.plurals.tokens_count, - count = type.tokensCount, - type.tokensCount, - ), - style = TangemTheme.typography.caption, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.End, - ) - } - - is UserWalletItem.Type.SingleCurrency -> Unit - } - } + ) } } } +@OptIn(ExperimentalComposeUiApi::class) +@Composable +private fun LoadedTokensInfo( + modifier: Modifier = Modifier, + balanceAmount: String, + tokensCount: Int?, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.End, + ) { + Text( + text = balanceAmount, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ) + if (tokensCount != null) { + SpacerH2() + Text( + text = pluralStringResource( + id = R.plurals.tokens_count, + count = tokensCount, + tokensCount, + ), + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + } +} + +@Composable +private fun LockedPlaceholder( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .background( + color = TangemTheme.colors.button.secondary, + shape = TangemTheme.shapes.roundedCornersMedium, + ), + ) { + Icon( + modifier = Modifier + .padding( + vertical = TangemTheme.dimens.spacing8, + horizontal = TangemTheme.dimens.spacing12, + ) + .size(TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_locked_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + } +} + @Composable private fun CardImagePlaceholder( modifier: Modifier = Modifier, ) { - Image( - modifier = modifier, - painter = painterResource(id = R.drawable.card_placeholder_black), - contentDescription = null, - ) + Box(modifier = modifier) { + Image( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.card_placeholder_black), + contentDescription = null, + ) + } +} + +@Composable +private fun CardImageShimmer( + modifier: Modifier = Modifier, +) { + Box(modifier = modifier.shimmer()) { + Box( + modifier = Modifier + .matchParentSize() + .background(TangemTheme.colors.button.secondary), + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 731400086c..5b111b5e96 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -31,7 +31,7 @@ data class TangemDimens internal constructor( val size0: Dp = 0.dp, val size4: Dp = 4.dp, val size8: Dp = 8.dp, - val size11: Dp = 11.dp, + val size12: Dp = 12.dp, val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, From d3e3f5cfd5459a15802d22f9a7059ec6026bfc86 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Dec 2022 21:02:04 +0300 Subject: [PATCH 10/16] Updated on 2026-08-14 --- .../ui/components/WalletItem.kt | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt index f33664bbfb..c95d59deec 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -7,11 +7,15 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -79,7 +83,16 @@ private fun WalletCardImage( .align(Alignment.Center) .width(TangemTheme.dimens.size56) .height(TangemTheme.dimens.size32) - .clip(TangemTheme.shapes.roundedCornersSmall) + .clip(TangemTheme.shapes.roundedCornersSmall2) + + val checkedColorFilter by rememberUpdatedState( + newValue = if (isChecked) { + ColorFilter.tint( + color = TangemTheme.colors.icon.accent.copy(alpha = .6f), + blendMode = BlendMode.SrcOver, + ) + } else null, + ) SubcomposeAsyncImage( modifier = cardImageModifier, @@ -89,46 +102,28 @@ private fun WalletCardImage( .build(), loading = { CardImageShimmer(modifier = cardImageModifier) }, error = { CardImagePlaceholder(modifier = cardImageModifier) }, + colorFilter = checkedColorFilter, contentDescription = null, ) - when { - isChecked -> { - Box( - modifier = cardImageModifier - .background( - color = TangemTheme.colors.icon.accent.copy(alpha = .5f), - ), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - tint = TangemTheme.colors.icon.primary2, - contentDescription = null, - ) - } + if (isSelected) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .size(TangemTheme.dimens.size18) + .background( + color = Color.White, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.padding(all = (0.5).dp), + painter = painterResource(id = R.drawable.ic_check_circle_18), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) } - isSelected -> { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .background( - color = Color.White, - shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .padding(all = (0.5).dp), - painter = painterResource(id = R.drawable.ic_check_circle_18), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } - } - else -> Unit } } } From a0f1a6591cc687aab4d44238ca4861374fd9dd99 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Dec 2022 14:40:31 +0300 Subject: [PATCH 11/16] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 12 + .../com/tangem/tap/common/redux/AppReducer.kt | 2 + .../com/tangem/tap/common/redux/AppState.kt | 2 + .../tap/domain/model/TotalFiatBalance.kt | 25 +++ .../com/tangem/tap/domain/model/UserWallet.kt | 32 +-- .../tap/domain/model/WalletDataModel.kt | 79 +++++++ .../tap/domain/model/WalletStoreModel.kt | 36 +++ .../model/builders/UserWalletBuilder.kt | 30 +++ .../TotalFiatBalanceCalculator.kt | 14 ++ .../di/TotalFiatBalanceCalculatorProvider.kt | 8 + .../DefaultTotalFiatBalanceCalculator.kt | 102 +++++++++ .../userWalletList/UserWalletsListManager.kt | 2 + ...r.kt => UserWalletsListManagerProvider.kt} | 0 .../DummyUserWalletsListManager.kt | 6 + .../walletStores/WalletStoresManager.kt | 27 +++ .../di/WalletsStoresManagerProvider.kt | 8 + .../DummyWalletStoresManager.kt | 35 +++ .../saveWallet/redux/SaveWalletMiddleware.kt | 17 +- .../walletSelector/redux/UserWalletModel.kt | 22 ++ .../redux/WalletSelectorAction.kt | 29 +++ .../redux/WalletSelectorMiddleware.kt | 212 ++++++++++++++++++ .../redux/WalletSelectorReducer.kt | 107 +++++++++ .../redux/WalletSelectorState.kt | 12 +- .../tap/features/walletSelector/ui/Mapper.kt | 76 +++++++ .../ui/WalletSelectorBottomSheetFragment.kt | 3 +- .../ui/WalletSelectorScreenState.kt | 4 +- .../ui/WalletSelectorViewModel.kt | 43 +++- .../welcome/redux/WelcomeMiddleware.kt | 4 +- 28 files changed, 905 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt rename app/src/main/java/com/tangem/tap/domain/userWalletList/di/{UserWalletsListProvider.kt => UserWalletsListManagerProvider.kt} (100%) create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/di/WalletsStoresManagerProvider.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 4b801de36a..82e2081d78 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -37,6 +37,10 @@ import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tokens.UserTokensRepository +import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator +import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation +import com.tangem.tap.domain.walletStores.WalletStoresManager +import com.tangem.tap.domain.walletStores.di.provideDummyImplementation import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.CardBalanceStateAdapter @@ -60,6 +64,14 @@ lateinit var shopService: TangemShopService lateinit var assetReader: AssetReader lateinit var userTokensRepository: UserTokensRepository +val walletStoresManager by lazy { + WalletStoresManager.provideDummyImplementation() +} + +val totalFiatBalanceCalculator by lazy { + TotalFiatBalanceCalculator.provideDefaultImplementation() +} + @HiltAndroidApp class TapApplication : Application(), ImageLoaderFactory { diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 6d2bf6af62..f03dfc70ca 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -16,6 +16,7 @@ import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.reducers.WalletReducer import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import org.rekotlin.Action @@ -40,6 +41,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) shopState = ShopReducer.reduce(action, state.shopState), welcomeState = WelcomeReducer.reduce(action, state), saveWalletState = SaveWalletReducer.reduce(action, state), + walletSelectorState = WalletSelectorReducer.reduce(action, state), ) } 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 d85b69df46..c89e7c0133 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 @@ -35,6 +35,7 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware +import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware import com.tangem.tap.features.walletSelector.redux.WalletSelectorState import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState @@ -88,6 +89,7 @@ data class AppState( ShopMiddleware().shopMiddleware, WelcomeMiddleware().middleware, SaveWalletMiddleware().middleware, + WalletSelectorMiddleware().middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt new file mode 100644 index 0000000000..6bdca4b2de --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/TotalFiatBalance.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.domain.model + +import java.math.BigDecimal + +/** + * Represents fiat balance of [WalletStoreModel] list + * @property amount Amount of the total balance + * */ +sealed class TotalFiatBalance { + open val amount: BigDecimal = BigDecimal.ZERO + + object Loading : TotalFiatBalance() + + class Refreshing( + override val amount: BigDecimal, + ) : TotalFiatBalance() + + class Error( + override val amount: BigDecimal, + ) : TotalFiatBalance() + + class Loaded( + override val amount: BigDecimal, + ) : TotalFiatBalance() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt index 6ce60fae85..4cb6fbfdd8 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt @@ -2,9 +2,17 @@ package com.tangem.tap.domain.model import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.util.UserWalletId -import com.tangem.domain.common.util.userWalletId -import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl +/** + * Represents user's wallet which stored in app persistence + * @param name User's wallet name + * @param walletId User's wallet [UserWalletId] + * @param artworkUrl User wallet card artwork URL + * @param cardsInWallet List of cards IDs assigned with this user's wallet + * @param scanResponse [ScanResponse] of primary user's wallet card. + * TODO: Replace with [com.tangem.domain.common.CardDTO] + * @property cardId ID of user's wallet primary card + * */ data class UserWallet( val name: String, val walletId: UserWalletId, @@ -14,24 +22,4 @@ data class UserWallet( ) { val cardId: String get() = scanResponse.card.cardId - - val hasAccessCode: Boolean - get() = scanResponse.card.isAccessCodeSet - - companion object { - suspend operator fun invoke( - scanResponse: ScanResponse, - backupCardsIds: Set? = null, - ): UserWallet { - return with(scanResponse) { - UserWallet( - walletId = card.userWalletId, - name = productType.name, - artworkUrl = card.getOrLoadCardArtworkUrl(), - cardsInWallet = backupCardsIds?.plus(card.cardId) ?: setOf(card.cardId), - scanResponse = this, - ) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt new file mode 100644 index 0000000000..173ed53786 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletDataModel.kt @@ -0,0 +1,79 @@ +package com.tangem.tap.domain.model + +import com.tangem.tap.domain.model.WalletDataModel.Status +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.redux.AddressData +import java.math.BigDecimal + +/** + * Contains info about wallet store currency + * @param currency Wallet's [Currency] + * @param status Wallet's [Status], represents current status of that currency + * @param walletAddresses List of wallet data [AddressData] + * @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all + * founds will be destroyed. Null if currency don't have existential deposit + * @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided + * */ +data class WalletDataModel( + val currency: Currency, + val status: Status, + val walletAddresses: List, + val existentialDeposit: BigDecimal?, + val fiatRate: BigDecimal?, +) { + + /** + * Represent current status of currency + * @property amount Currency amount + * @property pendingTransactions List of currency [PendingTransaction] sent in currency's blockchain + * @property errorMessage Status error message, null if not provided + * @property isErrorStatus true if current status is error status, false otherwise + * */ + sealed class Status { + open val amount: BigDecimal = BigDecimal.ZERO + open val pendingTransactions: List = emptyList() + open val errorMessage: String? = null + open val isErrorStatus: Boolean = false + } + + object Loading : Status() + + data class VerifiedOnline( + override val amount: BigDecimal, + ) : Status() + + data class TransactionInProgress( + override val amount: BigDecimal, + override val pendingTransactions: List, + ) : Status() + + data class SameCurrencyTransactionInProgress( + override val amount: BigDecimal, + override val pendingTransactions: List, + ) : Status() + + data class NoAccount( + val amountToCreateAccount: BigDecimal?, + ) : Status() { + override val isErrorStatus: Boolean = true + } + + data class Unreachable( + override val errorMessage: String?, + ) : Status() { + override val isErrorStatus: Boolean = true + } + + object MissedDerivation : Status() { + override val isErrorStatus: Boolean = true + } + + data class Refreshing( + override val amount: BigDecimal, + override val pendingTransactions: List, + override val errorMessage: String?, + ) : Status() { + override val isErrorStatus: Boolean = errorMessage != null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt new file mode 100644 index 0000000000..1753b9b0ba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/WalletStoreModel.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.domain.model + +import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.WalletStoreModel.WalletRent +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import java.math.BigDecimal + +/** + * Contains info about the blockchain and its currencies + * @param userWalletId ID of the [UserWallet] which uses that store + * @param blockchainNetwork Store's [BlockchainNetwork] + * @param walletManager Store's [WalletManager], may be null if it fails to create this manager + * @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies + * @param walletRent Store's [WalletRent], null if store has no rent or currency balance is greater then + * [WalletRent.exemptionAmount] + * */ +data class WalletStoreModel( + val userWalletId: UserWalletId, + val blockchainNetwork: BlockchainNetwork, + val walletManager: WalletManager?, + val walletsData: List, + val walletRent: WalletRent?, +) { + + /** + * Represents wallet blockchain rent + * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than + * the [WalletRent.exemptionAmount] + * @param exemptionAmount Amount that should be on the blockchain balance not to pay rent + * */ + data class WalletRent( + val rent: BigDecimal, + val exemptionAmount: BigDecimal, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt new file mode 100644 index 0000000000..4ea9db1491 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.domain.model.builders + +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.util.userWalletId +import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl +import com.tangem.tap.domain.model.UserWallet + +class UserWalletBuilder( + private val scanResponse: ScanResponse, +) { + private var backupCardsIds: Set = emptySet() + + fun setBackupCardsIds(backupCardsIds: Set?) = this.apply { + if (backupCardsIds != null) { + this.backupCardsIds = backupCardsIds + } + } + + suspend fun build(): UserWallet { + return with(scanResponse) { + UserWallet( + walletId = card.userWalletId, + name = productType.name, + artworkUrl = card.getOrLoadCardArtworkUrl(), + cardsInWallet = backupCardsIds.plus(card.cardId), + scanResponse = this, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt new file mode 100644 index 0000000000..d9b586d3e6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.domain.totalBalance + +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.domain.model.WalletStoreModel +import java.math.BigDecimal + +interface TotalFiatBalanceCalculator { + suspend fun calculate( + prevAmount: BigDecimal, + walletStores: List, + ): TotalFiatBalance + + companion object +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt new file mode 100644 index 0000000000..ea285cc404 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/di/TotalFiatBalanceCalculatorProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.totalBalance.di + +import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator +import com.tangem.tap.domain.totalBalance.implementation.DefaultTotalFiatBalanceCalculator + +fun TotalFiatBalanceCalculator.Companion.provideDefaultImplementation(): TotalFiatBalanceCalculator { + return DefaultTotalFiatBalanceCalculator() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt new file mode 100644 index 0000000000..be318667dd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt @@ -0,0 +1,102 @@ +package com.tangem.tap.domain.totalBalance.implementation + +import com.tangem.tap.common.extensions.toFiatValue +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.domain.model.WalletDataModel +import com.tangem.tap.domain.model.WalletStoreModel +import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { + override suspend fun calculate( + prevAmount: BigDecimal, + walletStores: List, + ): TotalFiatBalance { + return if (walletStores.isEmpty()) { + TotalFiatBalance.Loading + } else { + withContext(Dispatchers.Default) { + val walletsData = walletStores + .asSequence() + .flatMap { it.walletsData } + val calculateAmount = { walletsData.calculateTotalFiatAmount() } + + when (walletsData.findStatus()) { + TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading + TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing(prevAmount) + TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount()) + TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount()) + } + } + } + } + + private fun Sequence.findStatus(): TotalFiatBalanceStatus { + return this + .mapToStatus() + .reduce { prevStatus, newStatus -> + getCurrentStatus(prevStatus, newStatus) + } + } + + private fun Sequence.mapToStatus(): Sequence { + return this.map { walletData -> + when (walletData.status) { + is WalletDataModel.Refreshing -> TotalFiatBalanceStatus.Refreshing + is WalletDataModel.VerifiedOnline, + is WalletDataModel.SameCurrencyTransactionInProgress, + is WalletDataModel.TransactionInProgress, + is WalletDataModel.NoAccount, + -> TotalFiatBalanceStatus.Loaded + is WalletDataModel.Unreachable, + is WalletDataModel.MissedDerivation, + -> TotalFiatBalanceStatus.Error + is WalletDataModel.Loading -> TotalFiatBalanceStatus.Loading + } + } + } + + private fun Sequence.calculateTotalFiatAmount(): BigDecimal { + return this + .map { walletData -> + walletData.fiatRate + ?.let { walletData.status.amount.toFiatValue(it) } + ?: BigDecimal.ZERO + } + .reduce(BigDecimal::plus) + } + + private fun getCurrentStatus( + prevStatus: TotalFiatBalanceStatus, + newStatus: TotalFiatBalanceStatus, + ): TotalFiatBalanceStatus { + return when (prevStatus) { + TotalFiatBalanceStatus.Loading -> prevStatus + TotalFiatBalanceStatus.Refreshing -> when (newStatus) { + TotalFiatBalanceStatus.Loading -> prevStatus + TotalFiatBalanceStatus.Refreshing, + TotalFiatBalanceStatus.Error, + TotalFiatBalanceStatus.Loaded, + -> newStatus + } + TotalFiatBalanceStatus.Loaded, + TotalFiatBalanceStatus.Error, + -> when (newStatus) { + TotalFiatBalanceStatus.Loading, + TotalFiatBalanceStatus.Refreshing, + TotalFiatBalanceStatus.Error, + -> newStatus + TotalFiatBalanceStatus.Loaded -> prevStatus + } + } + } + + private enum class TotalFiatBalanceStatus { + Loading, + Refreshing, + Error, + Loaded, + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt index 05efbdf35f..3dc337ae61 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -25,5 +25,7 @@ interface UserWalletsListManager { suspend fun delete(walletIds: List): CompletionResult suspend fun clear(): CompletionResult + suspend fun get(walletId: UserWalletId): CompletionResult + companion object } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt similarity index 100% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListProvider.kt rename to app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt index bcc6c05b7f..5135966db1 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt @@ -55,4 +55,10 @@ class DummyUserWalletsListManager : UserWalletsListManager { override suspend fun clear(): CompletionResult { return CompletionResult.Success(Unit) } + + override suspend fun get(walletId: UserWalletId): CompletionResult { + return catching { + error("Not implemented") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt new file mode 100644 index 0000000000..346a6108e8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresManager.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.domain.walletStores + +import com.tangem.common.CompletionResult +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel +import kotlinx.coroutines.flow.Flow + +interface WalletStoresManager { + fun getAll(): Flow>> + fun get(userWalletId: UserWalletId): Flow> + + suspend fun delete(userWalletsIds: List): CompletionResult + suspend fun clear(): CompletionResult + + suspend fun fetch( + userWallet: UserWallet, + refresh: Boolean = false, + ): CompletionResult + + suspend fun fetch( + userWallets: List, + refresh: Boolean = false, + ): CompletionResult + + companion object +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/di/WalletsStoresManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/di/WalletsStoresManagerProvider.kt new file mode 100644 index 0000000000..fdc5187641 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/di/WalletsStoresManagerProvider.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.walletStores.di + +import com.tangem.tap.domain.walletStores.WalletStoresManager +import com.tangem.tap.domain.walletStores.implementation.DummyWalletStoresManager + +fun WalletStoresManager.Companion.provideDummyImplementation(): WalletStoresManager { + return DummyWalletStoresManager() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt new file mode 100644 index 0000000000..93c5c780c1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DummyWalletStoresManager.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.domain.walletStores.implementation + +import com.tangem.common.CompletionResult +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel +import com.tangem.tap.domain.walletStores.WalletStoresManager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +internal class DummyWalletStoresManager : WalletStoresManager { + override fun getAll(): Flow>> { + return emptyFlow() + } + + override fun get(userWalletId: UserWalletId): Flow> { + return emptyFlow() + } + + override suspend fun delete(userWalletsIds: List): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun clear(): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun fetch(userWallet: UserWallet, refresh: Boolean): CompletionResult { + return CompletionResult.Success(Unit) + } + + override suspend fun fetch(userWallets: List, refresh: Boolean): CompletionResult { + return CompletionResult.Success(Unit) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 844b55b115..500201104f 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.flatMap -import com.tangem.common.map import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store @@ -65,10 +65,9 @@ internal class SaveWalletMiddleware { ?: return scope.launch { - val userWallet = UserWallet( - scanResponse = scanResponse, - backupCardsIds = state.additionalInfo?.backupCardsIds, - ) + val userWallet = UserWalletBuilder(scanResponse) + .setBackupCardsIds(backupCardsIds = state.additionalInfo?.backupCardsIds) + .build() userWalletsListManager.save(userWallet) .flatMap { @@ -108,14 +107,6 @@ internal class SaveWalletMiddleware { cardsIds = userWallet.cardsInWallet, ) } - userWallet.hasAccessCode -> { - tangemSdkManager - .scanCard( - cardId = userWallet.cardId, - useBiometricsForAccessCode = true, - ) - .map { /* no-op */ } - } else -> { CompletionResult.Success(Unit) } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt new file mode 100644 index 0000000000..a1dc438e52 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/UserWalletModel.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.features.walletSelector.redux + +import com.tangem.tap.domain.model.TotalFiatBalance + +data class UserWalletModel( + val id: String, + val name: String, + val artworkUrl: String, + val type: Type, + val fiatBalance: TotalFiatBalance, +) { + sealed interface Type { + data class SingleCurrency( + val blockchainName: String?, + ) : Type + + data class MultiCurrency( + val tokensCount: Int, + val cardsInWallet: Int, + ) : Type + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt index 51a7253075..d18cf44dec 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorAction.kt @@ -1,9 +1,33 @@ package com.tangem.tap.features.walletSelector.redux import com.tangem.common.core.TangemError +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel import org.rekotlin.Action internal sealed interface WalletSelectorAction : Action { + data class UserWalletsLoaded( + val userWallets: List, + ) : WalletSelectorAction + + data class SelectedWalletChanged( + val selectedWallet: UserWallet, + ) : WalletSelectorAction + + data class IsLockedChanged( + val isLocked: Boolean, + ) : WalletSelectorAction + + data class WalletStoresChanged( + val walletsStores: Map>, + ) : WalletSelectorAction + + data class BalanceLoaded( + val userWalletModel: UserWalletModel, + ) : WalletSelectorAction + object UnlockWithBiometry : WalletSelectorAction { object Success : WalletSelectorAction data class Error(val error: TangemError) : WalletSelectorAction @@ -27,5 +51,10 @@ internal sealed interface WalletSelectorAction : Action { data class Error(val error: TangemError) : WalletSelectorAction } + data class ChangeAppCurrency( + val fiatCurrency: FiatCurrency, + ) : WalletSelectorAction + + data class HandleError(val error: TangemError) : WalletSelectorAction object CloseError : WalletSelectorAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt new file mode 100644 index 0000000000..00f5b77e25 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -0,0 +1,212 @@ +package com.tangem.tap.features.walletSelector.redux + +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.flatMap +import com.tangem.common.map +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.util.UserWalletId +import com.tangem.tap.* +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.WalletStoreModel +import com.tangem.tap.domain.model.builders.UserWalletBuilder +import com.tangem.tap.domain.scanCard.ScanCardProcessor +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.rekotlin.Middleware +import timber.log.Timber + +internal class WalletSelectorMiddleware { + val middleware: Middleware = { _, appStateProvider -> + { next -> + { action -> + val appState = appStateProvider() + if (action is WalletSelectorAction && appState != null) { + handleAction(action, appState.walletSelectorState) + } + next(action) + } + } + } + + private fun handleAction(action: WalletSelectorAction, state: WalletSelectorState) { + when (action) { + is WalletSelectorAction.UserWalletsLoaded -> { + fetchWalletStores(action.userWallets) + } + is WalletSelectorAction.WalletStoresChanged -> { + updateBalances(action.walletsStores, state) + } + is WalletSelectorAction.UnlockWithBiometry -> { + unlockWalletsWithBiometry() + } + is WalletSelectorAction.AddWallet -> { + addWallet() + } + is WalletSelectorAction.SelectWallet -> { + selectWallet(action.walletId) + } + is WalletSelectorAction.RemoveWallets -> { + removeWallets(action.walletIdsToRemove, state) + } + is WalletSelectorAction.RenameWallet -> { + renameWallet(action.walletId, action.newName) + } + is WalletSelectorAction.ChangeAppCurrency, + is WalletSelectorAction.AddWallet.Success, + is WalletSelectorAction.AddWallet.Error, + is WalletSelectorAction.SelectedWalletChanged, + is WalletSelectorAction.UnlockWithBiometry.Error, + is WalletSelectorAction.UnlockWithBiometry.Success, + is WalletSelectorAction.BalanceLoaded, + is WalletSelectorAction.IsLockedChanged, + is WalletSelectorAction.HandleError, + is WalletSelectorAction.CloseError, + -> Unit + } + } + + private fun fetchWalletStores(userWallets: List) { + scope.launch { + walletStoresManager.fetch(userWallets) + .doOnFailure { error -> + Timber.e(error, "Unable to fetch wallet stores") + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + } + } + + private fun updateBalances(walletStores: Map>, state: WalletSelectorState) { + walletStores.forEach { (walletId, walletStores) -> + scope.launch { + val updatedWallet = state.wallets + .find { it.id == walletId.stringValue } + ?.let { wallet -> + wallet.copy( + type = when (val type = wallet.type) { + is UserWalletModel.Type.MultiCurrency -> type.copy( + tokensCount = walletStores.flatMap { it.walletsData }.size, + ) + is UserWalletModel.Type.SingleCurrency -> type.copy( + blockchainName = walletStores + .firstOrNull() + ?.blockchainNetwork + ?.blockchain + ?.fullName, + ) + }, + fiatBalance = totalFiatBalanceCalculator.calculate( + prevAmount = wallet.fiatBalance.amount, + walletStores = walletStores, + ), + ) + } + + if (updatedWallet != null) { + store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet)) + } + } + } + } + + private fun unlockWalletsWithBiometry() { + scope.launch { + userWalletsListManager.unlockWithBiometry() + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Error(error)) + } + .doOnSuccess { + store.dispatchOnMain(WalletSelectorAction.UnlockWithBiometry.Success) + } + } + } + + private fun addWallet() = scope.launch { + scanCardInternal { scanResponse -> + val userWallet = UserWalletBuilder(scanResponse).build() + + userWalletsListManager.save(userWallet) + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) + } + .doOnSuccess { + val selectedWallet = userWalletsListManager.selectedUserWallet.first() + val isSavedWalletSelected = userWallet == selectedWallet + store.dispatchOnMain(WalletSelectorAction.AddWallet.Success) + if (isSavedWalletSelected) { + store.dispatchOnMain(NavigationAction.PopBackTo()) + store.onUserWalletSelected(selectedWallet) + } + } + } + } + + private fun selectWallet(id: String) { + scope.launch { + userWalletsListManager.selectWallet(UserWalletId(id)) + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + .doOnSuccess { selectedWallet -> + store.dispatchOnMain(NavigationAction.PopBackTo()) + store.onUserWalletSelected(selectedWallet) + } + } + } + + private fun removeWallets(walletIdsToRemove: List, state: WalletSelectorState) { + scope.launch { + when (walletIdsToRemove.size) { + state.wallets.size -> { // Removing all wallets + userWalletsListManager.clear() + .flatMap { walletStoresManager.clear() } + .doOnSuccess { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + } + } + + else -> { + userWalletsListManager.delete(walletIdsToRemove.map { UserWalletId(it) }) + .flatMap { walletStoresManager.delete(walletIdsToRemove) } + } + } + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + } + } + + private fun renameWallet(walletId: String, newName: String) { + scope.launch { + userWalletsListManager.get(walletId = UserWalletId(walletId)) + .map { it.copy(name = newName) } + .flatMap { userWalletsListManager.update(it) } + .doOnFailure { error -> + store.dispatchOnMain(WalletSelectorAction.HandleError(error)) + } + } + } + + private suspend inline fun scanCardInternal( + crossinline onCardScanned: suspend (ScanResponse) -> Unit, + ) { + ScanCardProcessor.scan( + useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + onSuccess = { + onCardScanned(it) + }, + onFailure = { error -> + store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) + }, + onWalletNotCreated = { + store.dispatchOnMain(WalletSelectorAction.AddWallet.Success) + store.dispatchOnMain(NavigationAction.PopBackTo()) + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt new file mode 100644 index 0000000000..82fe296fd4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt @@ -0,0 +1,107 @@ +package com.tangem.tap.features.walletSelector.redux + +import com.tangem.domain.common.CardDTO +import com.tangem.tap.common.extensions.replaceByOrAdd +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.extensions.isMultiwalletAllowed +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.domain.model.UserWallet +import org.rekotlin.Action + +internal object WalletSelectorReducer { + fun reduce(action: Action, state: AppState): WalletSelectorState { + return if (action is WalletSelectorAction) { + internalReduce(action, state.walletSelectorState) + } else state.walletSelectorState + } + + private fun internalReduce(action: WalletSelectorAction, state: WalletSelectorState): WalletSelectorState { + return when (action) { + is WalletSelectorAction.UserWalletsLoaded -> state.copy( + wallets = action.userWallets.updateWalletsModels(state.wallets), + ) + is WalletSelectorAction.SelectedWalletChanged -> state.copy( + selectedWalletId = action.selectedWallet.walletId.stringValue, + ) + is WalletSelectorAction.IsLockedChanged -> state.copy( + isLocked = action.isLocked, + ) + is WalletSelectorAction.BalanceLoaded -> state.copy( + wallets = state.wallets.updateWithBalance(action.userWalletModel), + ) + is WalletSelectorAction.HandleError -> state.copy(error = action.error) + is WalletSelectorAction.CloseError -> state.copy(error = null) + is WalletSelectorAction.UnlockWithBiometry -> state.copy( + isUnlockInProgress = true, + ) + is WalletSelectorAction.UnlockWithBiometry.Error -> state.copy( + isUnlockInProgress = false, + error = action.error, + ) + is WalletSelectorAction.UnlockWithBiometry.Success -> state.copy( + isUnlockInProgress = false, + ) + is WalletSelectorAction.AddWallet -> state.copy( + isCardSavingInProgress = true, + ) + is WalletSelectorAction.AddWallet.Error -> state.copy( + isCardSavingInProgress = false, + error = action.error, + ) + is WalletSelectorAction.AddWallet.Success -> state.copy( + isCardSavingInProgress = false, + ) + is WalletSelectorAction.ChangeAppCurrency -> state.copy( + fiatCurrency = action.fiatCurrency, + ) + is WalletSelectorAction.WalletStoresChanged, + is WalletSelectorAction.SelectWallet, + is WalletSelectorAction.RemoveWallets, + is WalletSelectorAction.RenameWallet, + -> state + } + } + + private fun List.updateWalletsModels(prevWallets: List): List { + return this.map { userWallet -> + prevWallets + .find { it.id == userWallet.walletId.stringValue } + ?.copy( + name = userWallet.name, + artworkUrl = userWallet.artworkUrl, + ) + ?: with(userWallet) { + UserWalletModel( + id = walletId.stringValue, + name = name, + artworkUrl = artworkUrl, + type = getType(), + fiatBalance = TotalFiatBalance.Loading, + ) + } + } + } + + private fun List.updateWithBalance( + userWalletModel: UserWalletModel, + ): List { + return ArrayList(this).apply { + replaceByOrAdd(userWalletModel) { it.id == userWalletModel.id } + } + } + + private fun UserWallet.getType(): UserWalletModel.Type { + return if (scanResponse.card.isMultiwalletAllowed) { + UserWalletModel.Type.MultiCurrency( + tokensCount = 0, + cardsInWallet = (scanResponse.card.backupStatus as? CardDTO.BackupStatus.Active) + ?.cardCount?.inc() + ?: 1, + ) + } else { + UserWalletModel.Type.SingleCurrency( + blockchainName = null, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt index c84839277a..df6ec0867d 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorState.kt @@ -1,5 +1,15 @@ package com.tangem.tap.features.walletSelector.redux +import com.tangem.common.core.TangemError +import com.tangem.tap.common.entities.FiatCurrency import org.rekotlin.StateType -class WalletSelectorState : StateType \ No newline at end of file +data class WalletSelectorState( + val wallets: List = emptyList(), + val selectedWalletId: String? = null, + val isLocked: Boolean = false, + val fiatCurrency: FiatCurrency = FiatCurrency.Default, + val isCardSavingInProgress: Boolean = false, + val isUnlockInProgress: Boolean = false, + val error: TangemError? = null, +) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt new file mode 100644 index 0000000000..34ce8d3ef9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/Mapper.kt @@ -0,0 +1,76 @@ +package com.tangem.tap.features.walletSelector.ui + +import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.common.extensions.toFormattedFiatValue +import com.tangem.tap.domain.model.TotalFiatBalance +import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.tap.features.walletSelector.redux.UserWalletModel +import com.tangem.tap.features.walletSelector.redux.WalletSelectorState +import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem +import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem +import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem + +internal fun WalletSelectorScreenState.updateWithNewState( + newState: WalletSelectorState, +): WalletSelectorScreenState { + val walletsUi = newState.wallets.toUiModels(newState.fiatCurrency) + val walletsIds = walletsUi.map { it.id } + val multiCurrencyWallets = arrayListOf() + val singleCurrencyWallets = arrayListOf() + walletsUi.forEach { wallet -> + when (wallet) { + is MultiCurrencyUserWalletItem -> { + multiCurrencyWallets.add(wallet) + } + is SingleCurrencyUserWalletItem -> { + singleCurrencyWallets.add(wallet) + } + } + } + + return this.copy( + multiCurrencyWallets = multiCurrencyWallets, + singleCurrencyWallets = singleCurrencyWallets, + selectedWalletId = newState.selectedWalletId, + editingWalletsIds = editingWalletsIds.filter { it in walletsIds }, + isLocked = newState.isLocked, + showUnlockProgress = newState.isUnlockInProgress, + showAddCardProgress = newState.isCardSavingInProgress, + error = newState.error + ?.takeUnless { it.silent } + ?.let { error -> + error.messageResId?.let { TextReference.Res(it) } + ?: TextReference.Str(error.customMessage) + }, + ) +} + +private fun List.toUiModels( + appCurrency: FiatCurrency, +): Sequence { + return this.asSequence().map { userWalletModel -> + with(userWalletModel) { + val balance = UserWalletItem.Balance( + amount = fiatBalance.amount.toFormattedFiatValue(appCurrency.symbol), + isLoading = fiatBalance is TotalFiatBalance.Loading, + ) + when (type) { + is UserWalletModel.Type.MultiCurrency -> MultiCurrencyUserWalletItem( + id = id, + name = name, + imageUrl = artworkUrl, + balance = balance, + cardsInWallet = type.cardsInWallet, + tokensCount = type.tokensCount, + ) + is UserWalletModel.Type.SingleCurrency -> SingleCurrencyUserWalletItem( + id = id, + name = name, + imageUrl = artworkUrl, + balance = balance, + tokenName = type.blockchainName ?: "—", + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt index 3637f2ea5e..50ee972e7d 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.fragment.app.viewModels import com.tangem.core.ui.fragments.ComposeBottomSheetFragment import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogContent import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent import com.tangem.tap.features.walletSelector.ui.model.RenameWalletDialog @@ -33,7 +34,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment + prevState.updateWithNewState(state) + } } override fun onCleared() { @@ -135,10 +144,38 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber + store.dispatchOnMain(WalletSelectorAction.WalletStoresChanged(walletStores)) + } + .launchIn(viewModelScope) + } + + private fun bootstrapAppFiatCurrency() { + store.dispatch( + WalletSelectorAction.ChangeAppCurrency( + fiatCurrency = store.state.globalState.appCurrency, + ), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index eb7fca3d46..73556a2d6f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -8,7 +8,7 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.model.UserWallet +import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.scope import com.tangem.tap.store @@ -58,7 +58,7 @@ internal class WelcomeMiddleware { private fun proceedWithCard() = scope.launch { scanCardInternal { scanResponse -> - val userWallet = UserWallet(scanResponse) + val userWallet = UserWalletBuilder(scanResponse).build() userWalletsListManager.unlockWithCard(userWallet) .doOnFailure { error -> From e993dc064760a7e6f3d16f456bead3c3ea246a95 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Dec 2022 14:40:31 +0300 Subject: [PATCH 12/16] Updated on 2026-08-14 --- .idea/codeStyles/Project.xml | 3 +- .idea/codeStyles/codeStyleConfig.xml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 4 +- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 6 +- app/src/main/assets/tangem-app-config | 2 +- .../com/tangem/tap/LockUserWalletsTimer.kt | 115 +++++ .../main/java/com/tangem/tap/MainActivity.kt | 19 +- .../java/com/tangem/tap/TapApplication.kt | 46 +- .../com/tangem/tap/common/extensions/Store.kt | 5 +- .../com/tangem/tap/common/redux/AppState.kt | 5 +- .../redux/LockUserWalletsTimerMiddleware.kt | 15 + .../common/redux/global/GlobalMidlleware.kt | 6 +- .../com/tangem/tap/domain/TangemSdkManager.kt | 54 ++- .../com/tangem/tap/domain/TapWalletManager.kt | 74 ++- .../configurable/config/ConfigManager.kt | 2 +- .../com/tangem/tap/domain/model/UserWallet.kt | 2 + .../tap/domain/model/WalletDataModel.kt | 6 + .../tap/domain/model/WalletStoreModel.kt | 4 +- .../model/builders/WalletStoreBuilder.kt | 77 ++++ .../tap/domain/scanCard/ScanCardProcessor.kt | 4 +- .../tasks/product/CreateProductWalletTask.kt | 2 + .../domain/tasks/product/ScanProductTask.kt | 3 + .../tap/domain/twins/TwinCardsManager.kt | 20 +- .../userWalletList/UserWalletListError.kt | 40 ++ .../di/UserWalletsListManagerProvider.kt | 68 ++- .../BiometricUserWalletsListManager.kt | 303 +++++++++++++ .../model/UserWalletEncryptionKey.kt | 33 ++ .../model/UserWalletInformation.kt | 20 + .../SelectedUserWalletRepository.kt | 8 + .../repository/UserWalletsKeysRepository.kt | 12 + .../UserWalletsPublicInformationRepository.kt | 15 + ...erWalletsSensitiveInformationRepository.kt | 16 + .../BiometricUserWalletsKeysRepository.kt | 106 +++++ .../DefaultSelectedUserWalletRepository.kt | 30 ++ ...tUserWalletsPublicInformationRepository.kt | 84 ++++ ...erWalletsSensitiveInformationRepository.kt | 142 ++++++ .../utils/ByteArrayListExtensions.kt | 9 + .../tap/domain/userWalletList/utils/Mapper.kt | 58 +++ .../utils/json/ByteArrayKeyAdapter.kt | 29 ++ .../utils/json/CardBackupStatusAdapter.kt | 57 +++ .../json/ExtendedPublicKeysMapAdapter.kt | 57 +++ .../json/ScanResponseDerivedKeysMapAdapter.kt | 52 +++ .../utils/json/WalletDerivedKeysMapAdapter.kt | 56 +++ .../WalletCurrenciesManager.kt | 30 ++ .../di/WalletCurrenciesManagerProvider.kt | 25 + .../DefaultWalletCurrenciesManager.kt | 205 +++++++++ .../domain/walletStores/WalletStoresError.kt | 39 ++ .../di/WalletsStoresManagerProvider.kt | 22 + .../DefaultWalletStoresManager.kt | 174 +++++++ .../utils/CompletitionsResultOperations.kt | 27 ++ .../repository/WalletAmountsRepository.kt | 26 ++ .../repository/WalletManagersRepository.kt | 25 + .../repository/WalletStoresRepository.kt | 30 ++ .../repository/di/RepositoryProvider.kt | 26 ++ .../DefaultWalletAmountsRepository.kt | 427 ++++++++++++++++++ .../DefaultWalletManagersRepository.kt | 192 ++++++++ .../DefaultWalletStoresRepository.kt | 91 ++++ .../utils/WalletDataOperations.kt | 181 ++++++++ .../utils/WalletStoreOperations.kt | 93 ++++ .../storage/WalletManagerStorage.kt | 36 ++ .../storage/WalletStoresStorage.kt | 51 +++ .../features/details/redux/DetailsAction.kt | 16 +- .../details/redux/DetailsMiddleware.kt | 161 ++++++- .../features/details/redux/DetailsReducer.kt | 19 +- .../features/details/redux/DetailsState.kt | 2 + .../ui/appsettings/AppSettingsScreen.kt | 29 +- .../ui/appsettings/AppSettingsScreenState.kt | 6 +- .../ui/appsettings/AppSettingsViewModel.kt | 13 + .../details/ui/details/DetailsScreenState.kt | 4 + .../details/ui/details/DetailsViewModel.kt | 6 +- .../tangem/tap/features/home/HomeFragment.kt | 10 +- .../tap/features/home/redux/HomeMiddleware.kt | 207 ++------- .../features/onboarding/OnboardingHelper.kt | 47 ++ .../note/redux/OnboardingNoteMiddleware.kt | 9 +- .../redux/OnboardingOtherCardsMiddleware.kt | 9 +- .../products/twins/redux/TwinCardsAction.kt | 5 +- .../twins/redux/TwinCardsMiddleware.kt | 36 +- .../products/twins/redux/TwinCardsState.kt | 17 +- .../products/twins/ui/TwinsCardsFragment.kt | 11 +- .../redux/OnboardingWalletMiddleware.kt | 12 +- .../saveWallet/redux/SaveWalletAction.kt | 2 +- .../saveWallet/redux/SaveWalletMiddleware.kt | 51 ++- .../saveWallet/redux/SaveWalletReducer.kt | 8 +- .../saveWallet/redux/SaveWalletState.kt | 4 +- .../ui/SaveWalletBottomSheetFragment.kt | 8 +- .../send/redux/middlewares/SendMiddleware.kt | 47 +- .../tap/features/send/ui/SendFragment.kt | 30 +- .../features/tokens/redux/TokensMiddleware.kt | 137 +++--- .../tap/features/wallet/redux/WalletAction.kt | 38 +- .../tap/features/wallet/redux/WalletState.kt | 16 +- .../middlewares/AppCurrencyMiddleware.kt | 14 +- .../middlewares/MultiWalletMiddleware.kt | 68 ++- .../redux/middlewares/WalletMiddleware.kt | 125 +++-- .../redux/middlewares/WarningsMiddleware.kt | 14 +- .../redux/reducers/MultiWalletReducer.kt | 20 +- .../wallet/redux/reducers/WalletReducer.kt | 189 +++++++- .../tap/features/wallet/ui/BalanceWidget.kt | 2 +- .../wallet/ui/WalletDetailsFragment.kt | 45 +- .../tap/features/wallet/ui/WalletFragment.kt | 24 +- .../tap/features/wallet/ui/WalletViewModel.kt | 39 ++ .../wallet/ui/adapters/WalletAdapter.kt | 25 +- .../wallet/ui/images/CurrencyIconView.kt | 2 + .../wallet/ui/wallet/MultiWalletView.kt | 16 +- .../wallet/ui/wallet/SingleWalletView.kt | 12 +- .../features/welcome/ui/WelcomeFragment.kt | 8 +- app/src/main/res/drawable/ic_tap_card_24.xml | 27 ++ .../drawable/shape_rectangle_rounded_100.xml | 2 +- .../res/layout/card_total_balance_shimmer.xml | 12 +- .../res/layout/layout_card_total_balance.xml | 132 +++--- app/src/main/res/values/styles.xml | 9 + buildSrc/src/main/java/Versions.kt | 2 +- .../com/tangem/core/ui/res/TangemShapes.kt | 5 +- .../res/values-it/strings_plurals_final.xml | 2 + .../src/main/res/values-ru/strings_final.xml | 2 + core/ui/src/main/res/values/strings_final.xml | 1 + .../java/com/tangem/domain/common/CardInfo.kt | 14 + .../domain/common/util/CardExtensions.kt | 20 +- .../tangem/domain/common/util/UserWalletId.kt | 6 + 119 files changed, 4449 insertions(+), 720 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt create mode 100644 app/src/main/java/com/tangem/tap/common/redux/LockUserWalletsTimerMiddleware.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletListError.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletEncryptionKey.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/SelectedUserWalletRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsSensitiveInformationRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultSelectedUserWalletRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/utils/ByteArrayListExtensions.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ByteArrayKeyAdapter.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ExtendedPublicKeysMapAdapter.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ScanResponseDerivedKeysMapAdapter.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/WalletDerivedKeysMapAdapter.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletCurrencies/WalletCurrenciesManager.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletCurrencies/di/WalletCurrenciesManagerProvider.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/WalletStoresError.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/implementation/utils/CompletitionsResultOperations.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletAmountsRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletManagersRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/WalletStoresRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/di/RepositoryProvider.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletStoresRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletStoreOperations.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletManagerStorage.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/walletStores/storage/WalletStoresStorage.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt create mode 100644 app/src/main/res/drawable/ic_tap_card_24.xml create mode 100644 domain/src/main/java/com/tangem/domain/common/CardInfo.kt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 76a59bafa3..6527894333 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -12,6 +12,7 @@