diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt new file mode 100644 index 0000000000..b51d1c0f08 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun BoxWithGradient( + modifier: Modifier = Modifier, + gradient: Brush = BottomGradient, + content: @Composable BoxScope.() -> Unit, +) { + val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) + + Box(modifier = modifier.fillMaxSize()) { + content() + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(TangemTheme.dimens.size164 + bottomInsetsPx.dp) + .background(gradient), + ) + } +} + +private val BottomGradient: Brush = Brush.verticalGradient( + colors = listOf( + TangemColorPalette.Black.copy(alpha = 0f), + TangemColorPalette.Black.copy(alpha = 0.75f), + TangemColorPalette.Black.copy(alpha = 0.95f), + TangemColorPalette.Black, + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index 685358db13..dd23347e63 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -1,23 +1,52 @@ package com.tangem.core.ui.components.appbar import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn 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.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.AdditionalButton -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * App bar with title and two additional buttons + * + * @param startButton button information attached to the left edge + * @param endButton button information attached to the right edge + * + * @see + * Figma Component + */ +@Composable +fun AppBarWithAdditionalButtons( + text: TextReference, + modifier: Modifier = Modifier, + startButton: AdditionalButton? = null, + endButton: AdditionalButton? = null, +) { + AppBarWithAdditionalButtons( + text = text.resolveReference(), + startButton = startButton, + endButton = endButton, + modifier = modifier, + ) +} /** * App bar with title and two additional buttons @@ -31,24 +60,33 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun AppBarWithAdditionalButtons( text: String, + modifier: Modifier = Modifier, startButton: AdditionalButton? = null, endButton: AdditionalButton? = null, ) { Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .heightIn(TangemTheme.dimens.size56) .padding(all = TangemTheme.dimens.spacing16), ) { if (startButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterStart), onClick = startButton.onIconClicked) { - Icon( - painter = painterResource(id = startButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = startButton.iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterStart) + .clickable( + onClick = startButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } Text( @@ -60,14 +98,23 @@ fun AppBarWithAdditionalButtons( ) if (endButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterEnd), onClick = endButton.onIconClicked) { - Icon( - painter = painterResource(id = endButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = endButton.iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterEnd) + .clickable( + onClick = endButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } } } @@ -87,6 +134,7 @@ private fun Preview_AppBarWithAdditionalButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -102,6 +150,7 @@ private fun Preview_AppBarWithOnlyStartButtons() { iconRes = R.drawable.ic_scan_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -117,6 +166,7 @@ private fun Preview_AppBarWithOnlyEndButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.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 55321fa0b2..863f3e59cc 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 @@ -83,6 +83,7 @@ data class TangemDimens internal constructor( val size96: Dp = 96.dp, val size102: Dp = 102.dp, val size108: Dp = 108.dp, + val size110: Dp = 110.dp, val size116: Dp = 116.dp, val size120: Dp = 120.dp, val size142: Dp = 142.dp, diff --git a/features/disclaimer/api/.gitignore b/features/disclaimer/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts new file mode 100644 index 0000000000..fcd117da1b --- /dev/null +++ b/features/disclaimer/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.disclaimer.api" +} + +dependencies { + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt new file mode 100644 index 0000000000..5575d38b51 --- /dev/null +++ b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt @@ -0,0 +1,12 @@ +package com.tangem.features.disclaimer.api + +import androidx.fragment.app.Fragment + +interface DisclaimerRouter { + + fun entryFragment(): Fragment + + companion object { + const val IS_TOS_ACCEPTED_KEY = "is_tos_accepted" + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/.gitignore b/features/disclaimer/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts new file mode 100644 index 0000000000..bc658e93f3 --- /dev/null +++ b/features/disclaimer/impl/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.disclaimer.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.appCompat) + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) + implementation(deps.compose.material3) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + + /** Domain modules */ + implementation(projects.domain.models) + implementation(projects.domain.card) + + /** Feature modules */ + implementation(projects.features.disclaimer.api) + + /** DI */ + implementation(deps.hilt.android) + implementation(project(":domain:settings")) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt new file mode 100644 index 0000000000..e0a30ce395 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt @@ -0,0 +1,58 @@ +package com.tangem.features.disclaimer.impl + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +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 +import javax.inject.Inject + +@AndroidEntryPoint +internal class DisclaimerFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + @Inject + lateinit var reduxNavController: ReduxNavController + + private val viewModel by viewModels() + + private val isTosAccepted: Boolean + get() = arguments?.getBoolean(DisclaimerRouter.IS_TOS_ACCEPTED_KEY) ?: false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.primary + BackHandler { + if (isTosAccepted) { + reduxNavController.popBackStack() + } else { + requireActivity().finish() + } + } + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + DisclaimerScreen(viewModel.state, reduxNavController::popBackStack) + } + + companion object { + /** Create disclaimer fragment instance */ + fun create(): DisclaimerFragment = DisclaimerFragment() + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt new file mode 100644 index 0000000000..8b3d34e029 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.disclaimer.impl.di + +import com.tangem.core.navigation.ReduxNavController +import com.tangem.features.disclaimer.api.DisclaimerRouter +import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [DisclaimerRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +object DisclaimerRouterModule { + + @Provides + @ActivityScoped + fun provideDisclaimerRouter(reduxNavController: ReduxNavController): DisclaimerRouter { + return DefaultDisclaimerRouter(reduxNavController) + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt new file mode 100644 index 0000000000..4860d3265d --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt @@ -0,0 +1,23 @@ +package com.tangem.features.disclaimer.impl.navigation + +import androidx.fragment.app.Fragment +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController +import com.tangem.features.disclaimer.impl.DisclaimerFragment +import javax.inject.Inject + +internal class DefaultDisclaimerRouter @Inject constructor( + private val reduxNavController: ReduxNavController, +) : InnerDisclaimerRouter { + + override fun entryFragment(): Fragment = DisclaimerFragment.create() + + override fun openPushNotificationPermission() { + // todo [REDACTED_JIRA] + } + + override fun openHome() { + reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt new file mode 100644 index 0000000000..5077db0901 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt @@ -0,0 +1,10 @@ +package com.tangem.features.disclaimer.impl.navigation + +import com.tangem.features.disclaimer.api.DisclaimerRouter + +internal interface InnerDisclaimerRouter : DisclaimerRouter { + + fun openPushNotificationPermission() + + fun openHome() +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt new file mode 100644 index 0000000000..01d0a398a6 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt @@ -0,0 +1,16 @@ +package com.tangem.features.disclaimer.impl.presentation.state + +internal data class DisclaimerState( + val url: String, + val isTosAccepted: Boolean, + val onAccept: (Boolean) -> Unit, +) + +internal object DummyDisclaimer { + + val state = DisclaimerState( + url = "https://tangem.com/tangem_tos.html", + isTosAccepted = false, + onAccept = {}, + ) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt new file mode 100644 index 0000000000..f99bd7bc4e --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt @@ -0,0 +1,174 @@ +package com.tangem.features.disclaimer.impl.presentation.ui + +import android.Manifest +import android.content.res.Configuration +import android.os.Build +import android.view.View +import android.view.ViewGroup +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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +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.extensions.resourceReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.disclaimer.impl.R +import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState +import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer + +@Composable +internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { + val bottomPadding = if (state.isTosAccepted) TangemTheme.dimens.spacing16 else TangemTheme.dimens.size64 + Box( + modifier = Modifier + .statusBarsPadding() + .background(TangemColorPalette.Dark6), + ) { + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = bottomPadding), + ) { + AppBarWithAdditionalButtons( + text = resourceReference(R.string.disclaimer_title), + startButton = AdditionalButton( + iconRes = R.drawable.ic_back_24, + onIconClicked = onBackClick, + ).takeIf { state.isTosAccepted }, + ) + DisclaimerContent(state.url) + } + if (!state.isTosAccepted) { + DisclaimerButton(state.onAccept) + } + } +} + +@Composable +private fun ColumnScope.DisclaimerContent(url: String) { + val transparent = Color.Transparent + + val progressState = remember { mutableStateOf(ProgressState.Loading) } + val webClient = remember { DisclaimerWebViewClient(progressState) } + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + AndroidView( + factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(transparent.toArgb()) + settings.allowFileAccess = false + settings.javaScriptEnabled = false + overScrollMode = View.OVER_SCROLL_NEVER + webViewClient = webClient + loadUrl(url) + } + }, + ) + + when (progressState.value) { + ProgressState.Loading -> CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, + modifier = Modifier + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + ) + ProgressState.Error -> 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 + } + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { + val shouldAskPushPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS).status.isGranted + } else { + true + } + val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(TangemTheme.dimens.size110 + bottomInsetsPx.dp) + .background( + Brush.verticalGradient( + colors = listOf( + TangemTheme.colors.background.primary.copy(alpha = 0f), + TangemTheme.colors.background.primary.copy(alpha = 0.75f), + TangemTheme.colors.background.primary.copy(alpha = 0.95f), + TangemTheme.colors.background.primary, + ), + ), + ), + ) + PrimaryButton( + text = stringResource(id = R.string.common_accept), + onClick = { onAccept(shouldAskPushPermission) }, + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DisclaimerScreen_Preview() { + TangemThemePreview { + DisclaimerScreen(state = DummyDisclaimer.state, onBackClick = {}) + } +} +// endregion \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt new file mode 100644 index 0000000000..348cad4107 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt @@ -0,0 +1,62 @@ +package com.tangem.features.disclaimer.impl.presentation.ui + +import android.graphics.Bitmap +import android.webkit.* +import androidx.compose.runtime.MutableState + +internal enum class ProgressState { + Loading, + Done, + Error, +} + +internal class DisclaimerWebViewClient(private val progressState: MutableState) : WebViewClient() { + + private var loadingUrl: String? = null + private var loadedUrl: String? = null + + fun reset() { + loadingUrl = null + loadedUrl = null + progressState.value = ProgressState.Loading + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + if (loadingUrl != url) progressState.value = ProgressState.Loading + loadingUrl = url + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + + if (loadedUrl != url) progressState.value = ProgressState.Done + loadedUrl = url + } + + override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) { + super.onReceivedError(view, resourceRequest, error) + error?.let { progressState.value = ProgressState.Error } + } + + override fun onReceivedHttpError( + view: WebView?, + resourceRequest: WebResourceRequest?, + errorResponse: WebResourceResponse?, + ) { + super.onReceivedHttpError(view, resourceRequest, errorResponse) + + if (resourceRequest != null && errorResponse != null) { + val isDifferentUrl = resourceRequest.url?.toString() != loadingUrl + val isSuccessCode = errorResponse.statusCode < RESPONSE_USER_ERROR_STATUS_CODE + val isNotDone = progressState.value != ProgressState.Done + if (isDifferentUrl || isSuccessCode || isNotDone) return + progressState.value = ProgressState.Error + } + } + + companion object { + private const val RESPONSE_USER_ERROR_STATUS_CODE = 400 + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt new file mode 100644 index 0000000000..d80a1236ed --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt @@ -0,0 +1,6 @@ +package com.tangem.features.disclaimer.impl.presentation.viewmodel + +internal interface DisclaimerClickIntents { + + fun onAccept(shouldAskPushPermission: Boolean) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt new file mode 100644 index 0000000000..bfc97b16d8 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt @@ -0,0 +1,42 @@ +package com.tangem.features.disclaimer.impl.presentation.viewmodel + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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 +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class DisclaimerViewModel @Inject constructor( + private val cardRepository: CardRepository, + private val disclaimerRouter: DefaultDisclaimerRouter, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, DisclaimerClickIntents { + + private val isTosAccepted: Boolean = savedStateHandle[IS_TOS_ACCEPTED_KEY] ?: false + + val state: DisclaimerState + get() = DisclaimerState( + onAccept = ::onAccept, + url = DISCLAIMER_URL, + isTosAccepted = isTosAccepted, + ) + + override fun onAccept(shouldAskPushPermission: Boolean) { + viewModelScope.launch { + cardRepository.acceptTangemTOS() + // todo [REDACTED_JIRA] + disclaimerRouter.openHome() + } + } + + private companion object { + const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" + } +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index c4f0245a43..7fb3f2a8f0 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -168,6 +168,7 @@ compose-navigation-hilt = { module = "androidx.hilt:hilt-navigation-compose", ve compose-accompanist-appCompatTheme = { module = "com.google.accompanist:accompanist-appcompat-theme", version.ref = "compose-accompanist" } compose-accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "compose-accompanist" } compose-accompanist-webView = { module = "com.google.accompanist:accompanist-webview", version.ref = "compose-accompanist" } +compose-accompanist-permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "compose-accompanist" } compose-paging = { module = "androidx.paging:paging-compose", version.ref = "compose-paging" } compose-reorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "compose-reorderable" } # endregion Compose diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e8f4f4945..82e7f8bef8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -176,6 +176,9 @@ include(":features:staking:impl") include(":features:details:api") include(":features:details:impl") + +include(":features:disclaimer:api") +include(":features:disclaimer:impl") // endregion Feature modules // region Domain modules