Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-19 21:11:19 +05:00
parent 5566331bdb
commit 76c5fd4032
16 changed files with 338 additions and 92 deletions

View file

@ -1,15 +1,12 @@
package com.tangem.tap
import android.Manifest
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.nfc.NfcAdapter
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.SystemBarStyle
@ -21,8 +18,6 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
@ -46,6 +41,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
@ -53,7 +49,9 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.disclaimer.api.DisclaimerRouter
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tester.api.TesterRouter
@ -67,6 +65,7 @@ import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.sdk.TangemSdkManager
@ -79,10 +78,10 @@ import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
@ -180,6 +179,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
internal lateinit var routingComponentFactory: RoutingComponent.Factory
@Inject
lateinit var disclaimerRouter: DisclaimerRouter
@Inject
lateinit var pushNotificationsRouter: PushNotificationsRouter
@Inject
lateinit var cardRepository: CardRepository
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -216,7 +224,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
installRouting()
initContent()
checkForNotificationPermission()
observeStateUpdates()
observePolkadotAccountHealthCheck()
@ -296,6 +303,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
qrScanningRouter = qrScanningRouter,
emailSender = emailSender,
stakingRouter = stakingRouter,
disclaimerRouter = disclaimerRouter,
pushNotificationsRouter = pushNotificationsRouter,
),
)
}
@ -527,8 +536,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent)))
}
} else {
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
lifecycleScope.launch {
val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles)
val isEnabled = toggles.isPushNotificationsEnabled && !cardRepository.isTangemTOSAccepted()
val route = if (isEnabled) AppRoute.Disclaimer(isTosAccepted = false) else AppRoute.Home
store.dispatchNavigationAction { replaceAll(route) }
intentProcessor.handleIntent(intentWhichStartedActivity, false)
}
}
@ -536,16 +549,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}
private fun checkForNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
BuildConfig.LOG_ENABLED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0)
}
}
private fun observePolkadotAccountHealthCheck() {
lifecycleScope.launch {
getPolkadotCheckHasResetUseCase()

View file

@ -59,7 +59,11 @@ internal object UseCaseScanProcessor {
),
)
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
add(DisclaimerChain(store, disclaimerWillShow))
val pushNotificationsToggles =
store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles)
if (pushNotificationsToggles.isPushNotificationsEnabled) {
add(DisclaimerChain(store, disclaimerWillShow))
}
add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager))
}

View file

@ -86,6 +86,7 @@ fun PrimaryButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
colors: ButtonColors = TangemButtonsDefaults.primaryButtonColors,
showProgress: Boolean = false,
enabled: Boolean = true,
) {
@ -94,7 +95,7 @@ fun PrimaryButton(
text = text,
icon = TangemButtonIconPosition.None,
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
colors = colors,
enabled = enabled,
showProgress = showProgress,
size = size,

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
@ -39,12 +40,16 @@ fun AppBarWithAdditionalButtons(
modifier: Modifier = Modifier,
startButton: AdditionalButton? = null,
endButton: AdditionalButton? = null,
textColor: Color = TangemTheme.colors.text.primary1,
iconColor: Color = TangemTheme.colors.icon.primary1,
) {
AppBarWithAdditionalButtons(
text = text.resolveReference(),
startButton = startButton,
endButton = endButton,
modifier = modifier,
textColor = textColor,
iconColor = iconColor,
)
}
@ -63,6 +68,8 @@ fun AppBarWithAdditionalButtons(
modifier: Modifier = Modifier,
startButton: AdditionalButton? = null,
endButton: AdditionalButton? = null,
textColor: Color = TangemTheme.colors.text.primary1,
iconColor: Color = TangemTheme.colors.icon.primary1,
) {
Box(
modifier = modifier
@ -73,7 +80,7 @@ fun AppBarWithAdditionalButtons(
Icon(
painter = painterResource(id = startButton.iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
tint = iconColor,
modifier = Modifier
.size(TangemTheme.dimens.size24)
.align(Alignment.CenterStart)
@ -92,7 +99,7 @@ fun AppBarWithAdditionalButtons(
Text(
text = text,
modifier = Modifier.align(Alignment.Center),
color = TangemTheme.colors.text.primary1,
color = textColor,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
@ -101,7 +108,7 @@ fun AppBarWithAdditionalButtons(
Icon(
painter = painterResource(id = endButton.iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
tint = iconColor,
modifier = Modifier
.size(TangemTheme.dimens.size24)
.align(Alignment.CenterEnd)

View file

@ -5,8 +5,4 @@ import androidx.fragment.app.Fragment
interface DisclaimerRouter {
fun entryFragment(): Fragment
companion object {
const val IS_TOS_ACCEPTED_KEY = "is_tos_accepted"
}
}

View file

@ -5,10 +5,10 @@ import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.common.routing.AppRoute.Disclaimer.Companion.IS_TOS_ACCEPTED_KEY
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.disclaimer.api.DisclaimerRouter
import com.tangem.features.disclaimer.impl.presentation.ui.DisclaimerScreen
import com.tangem.features.disclaimer.impl.presentation.viewmodel.DisclaimerViewModel
import dagger.hilt.android.AndroidEntryPoint
@ -26,7 +26,7 @@ internal class DisclaimerFragment : ComposeFragment() {
private val viewModel by viewModels<DisclaimerViewModel>()
private val isTosAccepted: Boolean
get() = arguments?.getBoolean(DisclaimerRouter.IS_TOS_ACCEPTED_KEY) ?: false
get() = arguments?.getBoolean(IS_TOS_ACCEPTED_KEY) ?: false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View file

@ -4,15 +4,16 @@ import androidx.fragment.app.Fragment
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.features.disclaimer.impl.DisclaimerFragment
import javax.inject.Inject
internal class DefaultDisclaimerRouter(
internal class DefaultDisclaimerRouter @Inject constructor(
private val appRouter: AppRouter,
) : InnerDisclaimerRouter {
override fun entryFragment(): Fragment = DisclaimerFragment.create()
override fun openPushNotificationPermission() {
// todo [REDACTED_JIRA]
appRouter.push(AppRoute.PushNotification)
}
override fun openHome() {

View file

@ -1,6 +1,7 @@
package com.tangem.features.disclaimer.impl.presentation.ui
import android.Manifest
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.os.Build
import android.view.View
@ -9,12 +10,12 @@ import android.webkit.WebView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
@ -28,6 +29,7 @@ import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons
import com.tangem.core.ui.components.appbar.models.AdditionalButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
@ -39,36 +41,37 @@ import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer
@Composable
internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val bottomPadding = if (state.isTosAccepted) {
bottomBarHeight + TangemTheme.dimens.size16
} else {
bottomBarHeight + TangemTheme.dimens.size64
}
val backgroundColor = if (state.isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6
val (textColor, iconColor) = if (state.isTosAccepted) {
TangemTheme.colors.text.primary1 to TangemTheme.colors.icon.primary1
} else {
TangemColorPalette.Light4 to TangemColorPalette.Light4
}
Box(
modifier = Modifier
.background(TangemColorPalette.Dark6)
.background(backgroundColor)
.statusBarsPadding(),
) {
Column {
Column(modifier = Modifier.padding(bottom = bottomPadding)) {
AppBarWithAdditionalButtons(
text = resourceReference(R.string.disclaimer_title),
startButton = AdditionalButton(
iconRes = R.drawable.ic_back_24,
onIconClicked = onBackClick,
).takeIf { state.isTosAccepted },
textColor = textColor,
iconColor = iconColor,
)
DisclaimerContent(state.url)
Spacer(
modifier = Modifier
.background(TangemColorPalette.Dark6)
.height(
height = if (!state.isTosAccepted) {
bottomBarHeight
} else {
bottomBarHeight + TangemTheme.dimens.size64
},
),
)
DisclaimerContent(state.url, state.isTosAccepted)
}
if (!state.isTosAccepted) {
BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = TangemColorPalette.Dark6)
BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = backgroundColor)
DisclaimerButton(state.onAccept)
} else {
NavigationBar3ButtonsScrim()
@ -76,10 +79,13 @@ internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) {
}
}
@SuppressLint("SetJavaScriptEnabled")
@Composable
private fun ColumnScope.DisclaimerContent(url: String) {
private fun DisclaimerContent(url: String, isTosAccepted: Boolean) {
val progressState = remember { mutableStateOf(ProgressState.Loading) }
val webClient = remember { DisclaimerWebViewClient(progressState) }
val transparent = Color.Transparent
val backgroundColor = if (isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6
Box(
modifier = Modifier,
) {
@ -90,12 +96,17 @@ private fun ColumnScope.DisclaimerContent(url: String) {
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
setBackgroundColor(TangemColorPalette.Dark6.toArgb())
setBackgroundColor(transparent.toArgb())
settings.allowFileAccess = false
settings.javaScriptEnabled = false
// to inject css style to display only in dark theme
settings.javaScriptEnabled = !isTosAccepted
overScrollMode = View.OVER_SCROLL_NEVER
webViewClient = webClient
clearHistory()
clearFormData()
clearCache(true)
loadUrl(url)
}
},
@ -103,7 +114,11 @@ private fun ColumnScope.DisclaimerContent(url: String) {
when (progressState.value) {
ProgressState.Loading -> {
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor),
) {
CircularProgressIndicator(
color = TangemTheme.colors.icon.informative,
modifier = Modifier
@ -112,25 +127,6 @@ private fun ColumnScope.DisclaimerContent(url: String) {
)
}
}
ProgressState.Error -> Box(modifier = Modifier.fillMaxSize()) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16)
.align(Alignment.Center),
) {
Text(
text = stringResource(id = R.string.disclaimer_error_loading),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.constantWhite,
)
PrimaryButton(
text = stringResource(R.string.common_retry),
onClick = { webClient.reset() },
)
}
}
else -> Unit
}
}
@ -147,6 +143,12 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
PrimaryButton(
text = stringResource(id = R.string.common_accept),
onClick = { onAccept(shouldAskPushPermission) },
colors = TangemButtonColors(
backgroundColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,
disabledBackgroundColor = TangemColorPalette.Light4,
disabledContentColor = TangemColorPalette.Dark6,
),
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()

View file

@ -10,6 +10,22 @@ internal enum class ProgressState {
Error,
}
/**
* Workaround to display web view with ToS only in dark theme
*/
private fun WebView.injectCSS() {
val code = "javascript:(function() {" +
"var node = document.createElement('style');" +
"node.type = 'text/css';" +
" node.innerHTML = 'body, label,th,p,a, td, tr,li,ul,span,table,h1,h2,h3,h4,h5,h6,h7,div,small {" +
" color: #C9C9C9;" +
"background-color: #1E1E1E;" +
" } ';" +
" document.head.appendChild(node);})();"
evaluateJavascript(code, null)
}
internal class DisclaimerWebViewClient(private val progressState: MutableState<ProgressState>) : WebViewClient() {
private var loadingUrl: String? = null
@ -22,20 +38,23 @@ internal class DisclaimerWebViewClient(private val progressState: MutableState<P
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
view?.injectCSS()
super.onPageStarted(view, url, favicon)
if (loadingUrl != url) progressState.value = ProgressState.Loading
if (loadingUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Loading
loadingUrl = url
}
override fun onPageFinished(view: WebView?, url: String?) {
view?.injectCSS()
super.onPageFinished(view, url)
if (loadedUrl != url) progressState.value = ProgressState.Done
if (loadedUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Done
loadedUrl = url
}
override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) {
view?.injectCSS()
super.onReceivedError(view, resourceRequest, error)
error?.let { progressState.value = ProgressState.Error }
}
@ -45,6 +64,7 @@ internal class DisclaimerWebViewClient(private val progressState: MutableState<P
resourceRequest: WebResourceRequest?,
errorResponse: WebResourceResponse?,
) {
view?.injectCSS()
super.onReceivedHttpError(view, resourceRequest, errorResponse)
if (resourceRequest != null && errorResponse != null) {

View file

@ -4,8 +4,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRoute.Disclaimer.Companion.IS_TOS_ACCEPTED_KEY
import com.tangem.domain.card.repository.CardRepository
import com.tangem.features.disclaimer.api.DisclaimerRouter.Companion.IS_TOS_ACCEPTED_KEY
import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter
import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState
import dagger.hilt.android.lifecycle.HiltViewModel
@ -31,8 +31,7 @@ internal class DisclaimerViewModel @Inject constructor(
override fun onAccept(shouldAskPushPermission: Boolean) {
viewModelScope.launch {
cardRepository.acceptTangemTOS()
// todo [REDACTED_JIRA]
disclaimerRouter.openHome()
disclaimerRouter.openPushNotificationPermission()
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
data class PushNotificationsBottomSheetConfig(
val isFirstTimeAsking: Boolean,
val onAllow: () -> Unit,
val onAllowed: () -> Unit,
val onDeny: () -> Unit,
val openSettings: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -37,6 +37,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
@ -53,6 +54,7 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.PushNotificationsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.common.*
@ -204,17 +206,7 @@ private fun WalletContent(
organizeTokens(state = selectedWallet, itemModifier = itemModifier)
}
val bottomSheetConfig = selectedWallet.bottomSheetConfig
if (bottomSheetConfig != null) {
when (bottomSheetConfig.content) {
is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig)
is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig)
is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig)
is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig)
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
}
}
ShowBottomSheet(bottomSheetConfig = selectedWallet.bottomSheetConfig)
WalletsListEffects(
lazyListState = walletsListState,
@ -606,6 +598,21 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi
}
}
@Composable
private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
if (bottomSheetConfig != null) {
when (bottomSheetConfig.content) {
is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig)
is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig)
is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig)
is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig)
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -0,0 +1,117 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components
import android.content.res.Configuration
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.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.showcase.ShowcaseContent
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<PushNotificationsBottomSheetConfig>(config = config) {
PushNotificationsSheetContent(content = it, onDismiss = config.onDismissRequest)
}
}
@Composable
private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) {
val isClicked = remember { mutableStateOf(false) }
val requestPushPermission = requestPushPermission(
isFirstTimeAsking = content.isFirstTimeAsking,
isClicked = isClicked,
onAllow = {
content.onAllowed()
onDismiss()
},
onDeny = {
content.onDeny()
onDismiss()
},
onOpenSettings = content.openSettings,
)
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
ShowcaseContent(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
iconRes = R.drawable.ic_rocket_launch_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_one),
),
ShowcaseItemModel(
iconRes = R.drawable.ic_storefront_24,
text = resourceReference(R.string.user_push_notification_agreement_argument_two),
),
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing40),
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing40,
bottom = TangemTheme.dimens.spacing16,
),
) {
SecondaryButton(
text = stringResource(R.string.common_later),
onClick = {
content.onDeny()
onDismiss()
},
modifier = Modifier.weight(1f),
)
PrimaryButton(
text = stringResource(R.string.common_allow),
onClick = {
isClicked.value = true
content.onAllow()
requestPushPermission()
},
modifier = Modifier.weight(1f),
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PushNotificationsSheetContent_Preview() {
TangemThemePreview {
PushNotificationsSheetContent(
PushNotificationsBottomSheetConfig(
isFirstTimeAsking = false,
onAllow = {},
onAllowed = {},
onDeny = {},
openSettings = {},
),
onDismiss = {},
)
}
}
// endregion

View file

@ -4,11 +4,11 @@ import androidx.compose.runtime.MutableState
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWal
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig
@ -28,6 +29,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSend
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.managetokens.navigation.ExpandableState
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -60,6 +62,10 @@ internal class WalletViewModel @Inject constructor(
private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender,
private val walletDeepLinksHandler: WalletDeepLinksHandler,
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val settingsManager: SettingsManager,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -80,6 +86,7 @@ internal class WalletViewModel @Inject constructor(
subscribeOnBalanceHiding()
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
}
private fun maybeMigrateNames() {
@ -145,6 +152,30 @@ internal class WalletViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun subscribeOnPushNotificationsPermission() {
viewModelScope.launch {
if (!shouldAskPermissionUseCase(PUSH_PERMISSION)) return@launch
withContext(dispatchers.io) { delay(timeMillis = 1_800) }
val isFirstTimeAsking = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true }
val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true }
val onDenyClick: () -> Unit = if (wasInitiallyAsk) {
clickIntents::onDelayAskPushPermission
} else {
clickIntents::onNeverAskPushPermission
}
stateHolder.showBottomSheet(
PushNotificationsBottomSheetConfig(
isFirstTimeAsking = isFirstTimeAsking,
onAllow = clickIntents::onAllowPushPermission,
onAllowed = clickIntents::onNeverAskPushPermission,
onDeny = onDenyClick,
openSettings = settingsManager::openSettings,
),
)
}
}
private fun subscribeOnSelectedWalletFlow() {
getSelectedWalletUseCase().onRight {
it

View file

@ -30,6 +30,7 @@ internal class WalletClickIntents @Inject constructor(
private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor,
private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor,
private val visaWalletIntentsImplementor: VisaWalletIntentsImplementor,
private val pushPermissionClickIntentsImplementor: WalletPushPermissionClickIntentsImplementor,
private val stateHolder: WalletStateController,
private val walletScreenContentLoader: WalletScreenContentLoader,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
@ -45,7 +46,8 @@ internal class WalletClickIntents @Inject constructor(
WalletWarningsClickIntents by warningsClickIntentsImplementer,
WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor,
WalletContentClickIntents by contentClickIntentsImplementor,
VisaWalletIntents by visaWalletIntentsImplementor {
VisaWalletIntents by visaWalletIntentsImplementor,
WalletPushPermissionClickIntents by pushPermissionClickIntentsImplementor {
override fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) {
super.initialize(router, coroutineScope)
@ -55,6 +57,7 @@ internal class WalletClickIntents @Inject constructor(
currencyActionsClickIntentsImplementor.initialize(router, coroutineScope)
contentClickIntentsImplementor.initialize(router, coroutineScope)
visaWalletIntentsImplementor.initialize(router, coroutineScope)
pushPermissionClickIntentsImplementor.initialize(router, coroutineScope)
}
fun onWalletChange(index: Int) {

View file

@ -0,0 +1,44 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import com.tangem.domain.settings.DelayPermissionRequestUseCase
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.launch
import javax.inject.Inject
internal interface WalletPushPermissionClickIntents {
fun onAllowPushPermission()
fun onDelayAskPushPermission()
fun onNeverAskPushPermission()
}
@ViewModelScoped
internal class WalletPushPermissionClickIntentsImplementor @Inject constructor(
private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase,
) : BaseWalletClickIntents(), WalletPushPermissionClickIntents {
override fun onAllowPushPermission() {
viewModelScope.launch {
setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION)
}
}
override fun onDelayAskPushPermission() {
viewModelScope.launch {
delayPermissionRequestUseCase(PUSH_PERMISSION)
}
}
override fun onNeverAskPushPermission() {
viewModelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
}
}
}