Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-22 18:18:01 +03:00
parent 57ac29d594
commit 404daaa085
6 changed files with 189 additions and 85 deletions

View file

@ -33,6 +33,7 @@
<application
android:name="com.tangem.tap.TangemHiltApplication"
android:theme="@style/AppTheme"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"

View file

@ -1,6 +1,6 @@
<resources>
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar" />
<style name="BaseAppTheme" parent="@style/Theme.AppCompat.DayNight.NoActionBar" />
<style name="AppTheme" parent="BaseAppTheme">
<item name="colorPrimary">@color/menu_accent_color</item>

View file

@ -0,0 +1,94 @@
package com.tangem.core.ui.components
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.core.ui.res.LocalIsInDarkTheme
@Composable
fun DialogFullScreen(
onDismissRequest: () -> Unit,
properties: DialogProperties = DialogProperties(),
content: @Composable () -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = properties.dismissOnBackPress,
dismissOnClickOutside = properties.dismissOnClickOutside,
securePolicy = properties.securePolicy,
usePlatformDefaultWidth = true, // must be true as a part of work around
decorFitsSystemWindows = false,
),
content = {
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
val parentView = LocalView.current.parent as View
SideEffect {
if (activityWindow != null && dialogWindow != null) {
val attributes = WindowManager.LayoutParams().apply {
copyFrom(activityWindow.attributes)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
} else {
flags = WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
}
type = dialogWindow.attributes.type
}
dialogWindow.attributes = attributes
parentView.layoutParams =
FrameLayout.LayoutParams(
activityWindow.decorView.width,
activityWindow.decorView.height,
)
}
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
val systemUiController = rememberSystemUiController(getActivityWindow())
val dialogSystemUiController = rememberSystemUiController(getDialogWindow())
SideEffect {
systemUiController.setSystemBarsColor(color = Color.Transparent)
dialogSystemUiController.setSystemBarsColor(color = Color.Transparent)
}
}
SystemBarsIconsDisposable(darkIcons = LocalIsInDarkTheme.current.not())
Surface(modifier = Modifier.fillMaxSize(), color = Color.Transparent) {
content()
}
},
)
}
// Window utils
@Composable
private fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
@Composable
private fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow()
private tailrec fun Context.getActivityWindow(): Window? = when (this) {
is Activity -> window
is ContextWrapper -> baseContext.getActivityWindow()
else -> null
}

View file

@ -5,7 +5,7 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.FullScreen
import com.tangem.core.ui.components.DialogFullScreen
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.ui.HotAccessCodeRequestFullScreenContent
@ -47,7 +47,7 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor(
var isShownIfProxy by remember { mutableStateOf(state.isShown) }
if (isShownIfProxy) {
FullScreen(focusable = true, onBackClick = state.onDismiss) {
DialogFullScreen(onDismissRequest = state.onDismiss) {
HotAccessCodeRequestFullScreenContent(
state = state.copy(isShown = isShownProxy),
modifier = modifier,

View file

@ -82,6 +82,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
it.copy(
accessCodeColor = PinTextColor.WrongCode,
onAccessCodeChange = {},
useBiometricVisible = currentRequest.hasBiometry,
)
}
delay(timeMillis = 500) // Delay to show the wrong access code state
@ -121,7 +122,10 @@ internal class HotAccessCodeRequestModel @Inject constructor(
if (accessCode.length == ACCESS_CODE_LENGTH) {
uiState.update {
it.copy(onAccessCodeChange = {})
it.copy(
onAccessCodeChange = {},
useBiometricVisible = false,
)
}
result.value = HotWalletPasswordRequester.Result.EnteredPassword(HotAuth.Password(accessCode.toCharArray()))

View file

@ -37,105 +37,110 @@ import com.tangem.features.hotwallet.impl.R
@Suppress("MagicNumber", "LongMethod")
@Composable
internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM, modifier: Modifier = Modifier) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),
) {
Column(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
horizontalAlignment = Alignment.CenterHorizontally,
Box(Modifier.fillMaxSize()) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),
) {
TangemTopAppBar(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onDismiss),
)
SpacerH(68.dp)
Column(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 24.dp),
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
text = stringResourceSafe(R.string.access_code_check_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onDismiss),
)
SpacerH24()
SpacerH(68.dp)
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
Column(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
text = stringResourceSafe(R.string.access_code_check_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
SpacerH(20.dp)
SpacerH24()
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,
pinTextColor = state.accessCodeColor,
onValueChange = state.onAccessCodeChange,
)
SpacerH(20.dp)
AnimatedVisibility(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
visible = state.wrongAccessCodeText != null,
enter = fadeIn(),
exit = fadeOut(),
) {
val wrongAccessCodeText =
state.wrongAccessCodeText ?: return@AnimatedVisibility
Text(
text = wrongAccessCodeText.resolveReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2.copy(
lineBreak = LineBreak.Heading,
),
color = TangemTheme.colors.text.warning,
)
}
}
AnimatedVisibility(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
visible = state.wrongAccessCodeText != null,
visible = state.useBiometricVisible,
enter = fadeIn(),
exit = fadeOut(),
) {
val wrongAccessCodeText =
state.wrongAccessCodeText ?: return@AnimatedVisibility
Text(
text = wrongAccessCodeText.resolveReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2.copy(
lineBreak = LineBreak.Heading,
SecondaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.navigationBarsPadding()
.imePadding(),
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResourceSafe(R.string.common_biometrics),
),
color = TangemTheme.colors.text.warning,
onClick = state.useBiometricClick,
)
}
}
if (state.useBiometricVisible) {
SecondaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.navigationBarsPadding()
.imePadding(),
text = stringResourceSafe(
id = R.string.welcome_unlock,
stringResourceSafe(R.string.common_biometrics),
),
onClick = state.useBiometricClick,
)
}
}
}