From 4b5adcd08ae4537a7c826ceaf62be66d72d52d27 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Sep 2023 13:26:01 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../features/details/redux/DetailsAction.kt | 9 + .../details/redux/DetailsMiddleware.kt | 131 ++++++-- .../features/details/redux/DetailsReducer.kt | 14 + .../features/details/redux/DetailsState.kt | 3 + .../ui/common/DetailsComposeElements.kt | 47 +-- .../details/ui/details/DetailsScreen.kt | 285 +++++++++++------- .../details/ui/details/DetailsScreenState.kt | 123 ++++++-- .../details/ui/details/DetailsViewModel.kt | 185 +++++++----- 8 files changed, 539 insertions(+), 258 deletions(-) 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 1e719df7c7..0b4facea41 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,5 +1,6 @@ package com.tangem.tap.features.details.redux +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.models.scan.CardDTO @@ -27,6 +28,14 @@ sealed class DetailsAction : Action { data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() object ResetCardSettingsData : DetailsAction() + object ScanAndSaveUserWallet : DetailsAction() { + + object Success : DetailsAction() + + data class Error(val error: TextReference?) : DetailsAction() + } + + object DismissError : DetailsAction() sealed class AccessCodeRecovery : DetailsAction() { object Open : AccessCodeRecovery() 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 6a72a5811a..aa3264275d 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 @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.redux import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess @@ -9,6 +10,9 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver @@ -19,6 +23,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain @@ -73,37 +78,8 @@ class DetailsMiddleware { store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) - DetailsAction.ScanCard -> { - scope.launch { - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) - .scan(allowsRequestAccessCodeFromRepository = true) - .doOnSuccess { scanResponse -> - // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards - store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) - val currentUserWalletId = state.scanResponse - ?.let { UserWalletIdBuilder.scanResponse(it).build() } - val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) - .build() - val isSameWallet = currentUserWalletId == scannedUserWalletId - - if (isSameWallet) { - store.dispatchOnMain( - DetailsAction.PrepareCardSettingsData( - scanResponse.card, - scanResponse.cardTypesResolver, - ), - ) - } else { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.error_wrong_wallet_tapped, - ), - ) - } - } - } - } + is DetailsAction.ScanCard -> scanCard(state) + is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet() } } @@ -460,4 +436,97 @@ class DetailsMiddleware { } } } + + private fun scanCard(state: DetailsState) = scope.launch { + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(allowsRequestAccessCodeFromRepository = true) + .doOnSuccess { scanResponse -> + // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards + store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) + val currentUserWalletId = state.scanResponse + ?.let { UserWalletIdBuilder.scanResponse(it).build() } + val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) + .build() + val isSameWallet = currentUserWalletId == scannedUserWalletId + + if (isSameWallet) { + store.dispatchOnMain( + DetailsAction.PrepareCardSettingsData( + scanResponse.card, + scanResponse.cardTypesResolver, + ), + ) + } else { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.common_warning, + messageId = R.string.error_wrong_wallet_tapped, + ), + ) + } + } + } + + private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) { + val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + + val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() + + // Update access code policy for access code saving when a card was scanned + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + ) + + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets), + onWalletNotCreated = { + // No need to rollback policy, continue with the policy set before the card scan + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + }, + disclaimerWillShow = { + store.dispatchOnMain(NavigationAction.PopBackTo()) + }, + onSuccess = { scanResponse -> + saveUserWalletAndPopBackToWalletScreen(scanResponse) + .doOnFailure { error -> + // Rollback policy if card saving was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to save user wallet") + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + } + }, + onFailure = { error -> + // Rollback policy if card scanning was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to scan card") + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + }, + ) + } + + private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult { + val userWallet = UserWalletBuilder(scanResponse).build() + ?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + + return userWalletsListManager.save(userWallet) + .doOnSuccess { + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + + val walletFeatureToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + store.onUserWalletSelected(userWallet) + } + } + } + + private fun TangemError.toTextReference(): TextReference? { + if (silent) return null + + return messageResId?.let(::resourceReference) ?: stringReference(customMessage) + } } \ 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 1b70773cc1..b2754c775e 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 @@ -15,6 +15,7 @@ object DetailsReducer { fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) } +@Suppress("CyclomaticComplexMethod") private fun internalReduce(action: Action, state: AppState): DetailsState { if (action !is DetailsAction) return state.detailsState val detailsState = state.detailsState @@ -45,6 +46,19 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { ), ) is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState) + is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy( + isScanningInProgress = true, + ) + is DetailsAction.ScanAndSaveUserWallet.Error -> detailsState.copy( + isScanningInProgress = false, + error = action.error, + ) + is DetailsAction.ScanAndSaveUserWallet.Success -> detailsState.copy( + isScanningInProgress = false, + ) + is DetailsAction.DismissError -> detailsState.copy( + error = null, + ) else -> detailsState } } 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 14e533ed54..f37f6fed94 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 @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -13,6 +14,8 @@ data class DetailsState( val cardSettingsState: CardSettingsState? = null, val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, + val isScanningInProgress: Boolean = false, + val error: TextReference? = null, val appSettingsState: AppSettingsState = AppSettingsState(), ) : StateType diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index 653c54b272..bdba522a2b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource @@ -22,9 +23,11 @@ internal fun SettingsScreensScaffold( onBackClick: () -> Unit, content: @Composable () -> Unit, modifier: Modifier = Modifier, - fab: @Composable (() -> Unit)? = null, @StringRes titleRes: Int? = null, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + fab: @Composable () -> Unit = {}, ) { + val state = rememberScaffoldState(snackbarHostState = snackbarHostState) val backgroundColor = TangemTheme.colors.background.secondary BackHandler(onBack = onBackClick) @@ -33,6 +36,7 @@ internal fun SettingsScreensScaffold( } Scaffold( + scaffoldState = state, topBar = { EmptyTopBarWithNavigation( onBackClick = onBackClick, @@ -41,27 +45,28 @@ internal fun SettingsScreensScaffold( }, modifier = modifier.systemBarsPadding(), backgroundColor = backgroundColor, - floatingActionButton = { fab?.invoke() }, - ) { paddings -> - Column( - modifier = Modifier - .padding(paddings) - .fillMaxSize(), - ) { - if (titleRes != null) { - Text( - text = stringResource(id = titleRes), - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing20) - .padding(bottom = TangemTheme.dimens.spacing36), - style = TangemTheme.typography.h1, - color = TangemTheme.colors.text.primary1, - ) - } + floatingActionButton = fab, + content = { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize(), + ) { + if (titleRes != null) { + Text( + text = stringResource(id = titleRes), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing20) + .padding(bottom = TangemTheme.dimens.spacing36), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + } - content() - } - } + content() + } + }, + ) } @Composable diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index 2326b3db65..1d074d274a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -6,36 +6,44 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material.* +import androidx.compose.runtime.* 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.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.toImmutableList @Composable internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + val snackbarHostState = remember { SnackbarHostState() } + SettingsScreensScaffold( modifier = modifier, + snackbarHostState = snackbarHostState, content = { Content(state = state) }, onBackClick = onBackClick, ) + + ShowSnackbarIfNeeded( + snackbarHostState = snackbarHostState, + messageEvent = state.showSnackbar, + ) } @Composable @@ -50,102 +58,124 @@ private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) { SpacerH(height = TangemTheme.dimens.spacing36) SettingsItems( items = state.elements, - onItemsClick = state.onItemsClick, ) SpacerHMax() TangemSocialAccounts( links = state.tangemLinks, onSocialNetworkClick = state.onSocialNetworkClick, ) - SpacerH(height = TangemTheme.dimens.spacing16) + SpacerH(height = TangemTheme.dimens.spacing12) TangemAppVersion( appNameRes = state.appNameRes, version = state.tangemVersion, ) - SpacerH(height = TangemTheme.dimens.spacing24) + SpacerH(height = TangemTheme.dimens.spacing16) } - ShowSnackbarIfNeeded(state.showErrorSnackbar.value) } } @Composable -private fun SettingsItems(items: List, onItemsClick: (SettingsElement) -> Unit) { +private fun SettingsItems(items: List) { items.forEach { item -> - val onItemClick = remember(item) { - { onItemsClick(item) } - } - - if (item == SettingsElement.WalletConnect) { - WalletConnectDetailsItem(onItemClick) + if (item.isLarge) { + LargeDetailsItem(item) } else { - DetailsItem( - item = item, - onItemClick = onItemClick, - ) + DetailsItem(item) } } } @Composable -private fun WalletConnectDetailsItem(onItemClick: () -> Unit) { +private fun LargeDetailsItem(item: SettingsItem) { Row( modifier = Modifier - .defaultMinSize(minHeight = 84.dp) - .fillMaxWidth() - .clickable(onClick = onItemClick), - horizontalArrangement = Arrangement.Start, + .clickable(onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size84) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = R.drawable.ic_walletconnect), - contentDescription = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = TangemColorPalette.Azure, - ) + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemColorPalette.Azure, + ) + } Column( - modifier = Modifier.defaultMinSize(minHeight = 56.dp), + modifier = Modifier.heightIn(min = TangemTheme.dimens.size56), horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing4, + alignment = Alignment.CenterVertically, + ), ) { Text( - text = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), + text = item.title.resolveReference(), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) - Text( - text = stringResource(id = R.string.wallet_connect_subtitle), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - ) + + if (item.subtitle != null) { + Text( + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + ) + } } } } @Composable -private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) { +private fun DetailsItem(item: SettingsItem) { Row( modifier = Modifier - .height(56.dp) - .fillMaxWidth() - .clickable(onClick = onItemClick), - horizontalArrangement = Arrangement.Start, + .clickable(enabled = !item.showProgress, onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size56) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = item.iconRes), - contentDescription = stringResource(id = item.titleRes), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = TangemTheme.colors.icon.secondary, - ) - Column(modifier = Modifier.padding(end = 20.dp)) { + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemTheme.colors.icon.secondary, + ) + } + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceAround, + ) { Text( - text = stringResource(id = item.titleRes), - modifier = Modifier, + text = item.title.resolveReference(), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) + + if (item.subtitle != null) { + Text( + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } } } } @@ -153,50 +183,45 @@ private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) { @Composable private fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { LazyRow( - modifier = Modifier.padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing8), ) { items(links) { - Icon( - painter = painterResource(id = it.network.iconRes), - contentDescription = "", + val onClick = remember(it) { + { onSocialNetworkClick(it) } + } + + IconButton( modifier = Modifier - .padding(8.dp) - .clickable { onSocialNetworkClick(it) }, - tint = TangemTheme.colors.icon.informative, - ) + .padding(horizontal = TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size32), + onClick = onClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = it.network.iconRes), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } } } } @Composable -private fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - SnackbarHost( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(vertical = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - hostState = snackbarHostState, - ) - val errorTitle = when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> stringResource(id = R.string.alert_demo_feature_disabled) - EventError.Empty -> "" - } - if (snackbarErrorState != EventError.Empty) { - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar(errorTitle) - } - when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> snackbarErrorState.onErrorShow.invoke() - else -> { - /*no-op*/ - } - } +private fun ShowSnackbarIfNeeded(snackbarHostState: SnackbarHostState, messageEvent: StateEvent) { + var message: TextReference? by remember { mutableStateOf(value = null) } + val resolvedMessage by rememberUpdatedState(newValue = message?.resolveReference()) + + LaunchedEffect(resolvedMessage) { + resolvedMessage?.let { + snackbarHostState.showSnackbar(it) } } + + EventEffect(messageEvent) { + message = it + } } @Composable @@ -210,33 +235,59 @@ private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifie } // region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 900) @Composable -private fun DetailsScreenContentSample() { - DetailsScreen( - state = DetailsScreenState( - elements = SettingsElement.values().toList(), +private fun DetailsScreenPreview_Light( + @PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState, +) { + TangemTheme(isDark = false) { + DetailsScreen(param, onBackClick = {}) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 900) +@Composable +private fun DetailsScreenPreview_Dark(@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState) { + TangemTheme(isDark = true) { + DetailsScreen(param, onBackClick = {}) + } +} + +private class DetailsScreenStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = false, {}).let(::add) + SettingsItem.LinkMoreCards({}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.ReferralProgram({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), tangemLinks = TangemSocialAccounts.accountsEn, tangemVersion = "Tangem 2.14.12 (343)", - onItemsClick = {}, + showSnackbar = consumedEvent(), onSocialNetworkClick = {}, - ), - onBackClick = {}, - ) -} + ).let(::add) -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun DetailsScreenContentPreview_Light() { - TangemTheme(isDark = false) { - DetailsScreenContentSample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun DetailsScreenContentPreview_Dark() { - TangemTheme(isDark = true) { - DetailsScreenContentSample() - } -} + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = true, {}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), + tangemLinks = TangemSocialAccounts.accountsRu, + tangemVersion = "Tangem 2.14.12 (343)", + showSnackbar = consumedEvent(), + onSocialNetworkClick = {}, + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index f6ea3f1f19..a9d511210e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -1,37 +1,118 @@ package com.tangem.tap.features.details.ui.details +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Immutable internal data class DetailsScreenState( - val elements: List, - val tangemLinks: List, + val elements: ImmutableList, + val tangemLinks: ImmutableList, val tangemVersion: String, - val onItemsClick: (SettingsElement) -> Unit, + val showSnackbar: StateEvent, val onSocialNetworkClick: (SocialNetworkLink) -> Unit, - val showErrorSnackbar: MutableState = mutableStateOf(EventError.Empty), ) { val appNameRes: Int = R.string.tangem_app_name } @Immutable -internal enum class SettingsElement( - val iconRes: Int, - val titleRes: Int, +internal sealed class SettingsItem( + @DrawableRes val iconResId: Int, + val title: TextReference, + val subtitle: TextReference? = null, + val isLarge: Boolean = false, ) { - WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title), - LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), - ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title), - CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title), - AppSettings(R.drawable.ic_settings, R.string.app_settings_title), - Chat(R.drawable.ic_chat, R.string.details_chat), - SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback), - TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App, - PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy), - TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu), + + abstract val onClick: () -> Unit + + open val showProgress: Boolean = false + + data class WalletConnect( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_walletconnect, + title = resourceReference(R.string.wallet_connect_title), + subtitle = resourceReference(R.string.wallet_connect_subtitle), + isLarge = true, + ) + + data class AddWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = stringReference(value = "Add new wallet"), + ) + + data class ScanWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = stringReference(value = "Scan new wallet"), + ) + + data class LinkMoreCards( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_more_cards, + title = resourceReference(R.string.details_row_title_create_backup), + ) + + data class CardSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_card_settings, + title = resourceReference(R.string.card_settings_title), + ) + + data class AppSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_settings, + title = resourceReference(R.string.app_settings_title), + ) + + data class Chat( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_chat, + title = resourceReference(R.string.details_chat), + ) + + data class SendFeedback( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_comment, + title = resourceReference(R.string.details_row_title_send_feedback), + ) + + data class ReferralProgram( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_add_friends, + title = resourceReference(R.string.details_referral_title), + ) + + data class TermsOfService( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_text, + title = resourceReference(R.string.disclaimer_title), + ) + + data class TesterMenu( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_alert_24, + title = resourceReference(R.string.tester_menu), + ) } @Immutable @@ -57,7 +138,7 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) { } internal object TangemSocialAccounts { - val accountsEn: List = listOf( + val accountsEn: ImmutableList = persistentListOf( SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"), SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), @@ -67,7 +148,7 @@ internal object TangemSocialAccounts { SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"), SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"), ) - val accountsRu: List = listOf( + val accountsRu: ImmutableList = persistentListOf( SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"), SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 7877dff881..29ab390def 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -5,6 +5,10 @@ import androidx.compose.runtime.mutableStateOf import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchWithMain @@ -21,6 +25,9 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn @@ -42,82 +49,64 @@ internal class DetailsViewModel(private val store: Store) { elements = createSettingsItems(state), tangemLinks = getSocialLinks(), tangemVersion = getTangemAppVersion(), - onItemsClick = { handleClickingSettingsItem(it) }, - onSocialNetworkClick = { handleSocialNetworkClick(it) }, + showSnackbar = triggerErrorSnackbarIfNeeded(state.error), + onSocialNetworkClick = ::handleSocialNetworkClick, ) } - @Suppress("ComplexMethod") - private fun createSettingsItems(state: DetailsState): List { - val scanResponse = state.scanResponse ?: return emptyList() + private fun createSettingsItems(state: DetailsState): ImmutableList { + val scanResponse = state.scanResponse ?: return persistentListOf() val cardTypesResolver = scanResponse.cardTypesResolver - return SettingsElement.values().mapNotNull { - when (it) { - SettingsElement.WalletConnect -> if (cardTypesResolver.isMultiwalletAllowed()) it else null - SettingsElement.SendFeedback -> it - SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null - SettingsElement.PrivacyPolicy -> if (state.privacyPolicyUrl != null) it else null - SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null - SettingsElement.ReferralProgram -> if (cardTypesResolver.isTangemWallet()) it else null - SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null - else -> it - } - } + return buildList { + SettingsItem.WalletConnect(::navigateToWalletConnect) + .takeIf { cardTypesResolver.isMultiwalletAllowed() } + ?.let(::add) + + SettingsItem.AddWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeIf { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.ScanWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeUnless { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.LinkMoreCards(::linkMoreCards) + .takeIf { state.createBackupAllowed } + ?.let(::add) + + SettingsItem.CardSettings(::navigateToCardSettings) + .let(::add) + + SettingsItem.AppSettings(::navigateToAppSettings) + .let(::add) + + SettingsItem.Chat(::navigateToChat) + .let(::add) + + SettingsItem.SendFeedback(::sendFeedback) + .let(::add) + + SettingsItem.ReferralProgram(::navigateToReferralProgram) + .takeIf { cardTypesResolver.isTangemWallet() } + ?.let(::add) + + SettingsItem.TermsOfService(::navigateToToS) + .let(::add) + + SettingsItem.TesterMenu(::navigateToTesterMenu) + .takeIf { BuildConfig.TESTER_MENU_ENABLED } + ?.let(::add) + }.toImmutableList() } - private fun handleSocialNetworkClick(link: SocialNetworkLink) { - Analytics.send(Settings.ButtonSocialNetwork(link.network)) - store.dispatch(NavigationAction.OpenUrl(link.url)) - } - - private fun handleClickingSettingsItem(item: SettingsElement) { - when (item) { - SettingsElement.WalletConnect -> { - Analytics.send(Settings.ButtonWalletConnect()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) - } - SettingsElement.Chat -> { - Analytics.send(Settings.ButtonChat()) - store.dispatch(GlobalAction.OpenChat(SupportInfo())) - } - SettingsElement.SendFeedback -> { - Analytics.send(Settings.ButtonSendFeedback()) - store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) - } - SettingsElement.CardSettings -> { - Analytics.send(Settings.ButtonCardSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) - } - SettingsElement.AppSettings -> { - Analytics.send(Settings.ButtonAppSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) - } - SettingsElement.LinkMoreCards -> { - Analytics.send(Settings.ButtonCreateBackup()) - store.dispatch(WalletAction.MultiWallet.BackupWallet) - } - SettingsElement.TermsOfService -> { - store.dispatch(DisclaimerAction.Show(AppScreen.Details)) - } - SettingsElement.PrivacyPolicy -> { - // TODO: To be available later - } - SettingsElement.ReferralProgram -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) - } - SettingsElement.TesterMenu -> { - store.state.daggerGraphState.testerRouter?.startTesterScreen() - } - } - } - - private fun getSocialLinks(): List { - val locale = LocaleRegionProvider().getRegion() - return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { - TangemSocialAccounts.accountsRu + private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent { + return if (text == null) { + consumedEvent() } else { - TangemSocialAccounts.accountsEn + triggeredEvent(text) { + store.dispatch(DetailsAction.DismissError) + } } } @@ -127,6 +116,66 @@ internal class DetailsViewModel(private val store: Store) { return "$versionName ($versionCode)" } + private fun navigateToTesterMenu() { + store.state.daggerGraphState.testerRouter?.startTesterScreen() + } + + private fun navigateToToS() { + store.dispatch(DisclaimerAction.Show(AppScreen.Details)) + } + + private fun navigateToReferralProgram() { + store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) + } + + private fun sendFeedback() { + Analytics.send(Settings.ButtonSendFeedback()) + store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + } + + private fun navigateToChat() { + Analytics.send(Settings.ButtonChat()) + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + + private fun navigateToAppSettings() { + Analytics.send(Settings.ButtonAppSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) + } + + private fun navigateToCardSettings() { + Analytics.send(Settings.ButtonCardSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) + } + + private fun linkMoreCards() { + Analytics.send(Settings.ButtonCreateBackup()) + store.dispatch(WalletAction.MultiWallet.BackupWallet) + } + + private fun scanAndSaveUserWallet() { + store.dispatch(DetailsAction.ScanAndSaveUserWallet) + } + + private fun navigateToWalletConnect() { + Analytics.send(Settings.ButtonWalletConnect()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) + } + + private fun handleSocialNetworkClick(link: SocialNetworkLink) { + Analytics.send(Settings.ButtonSocialNetwork(link.network)) + store.dispatch(NavigationAction.OpenUrl(link.url)) + } + + private fun getSocialLinks(): ImmutableList { + val locale = LocaleRegionProvider().getRegion() + return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { + TangemSocialAccounts.accountsRu + } else { + TangemSocialAccounts.accountsEn + } + } + private fun bootstrapScreenState() { userWalletsListManager.selectedUserWallet .distinctUntilChanged()