Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-16 13:45:02 +03:00
parent ba28cf5b6d
commit 7bbab0a40b
14 changed files with 212 additions and 4 deletions

View file

@ -0,0 +1,11 @@
package com.tangem.tap.core.ui
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.ui.DesignFeatureToggles
import javax.inject.Inject
class DefaultDesignFeatureToggles @Inject constructor(
featureTogglesManager: FeatureTogglesManager,
) : DesignFeatureToggles {
override val isRedesignEnabled: Boolean = featureTogglesManager.isFeatureEnabled("APP_REDESIGN_ENABLED")
}

View file

@ -4,6 +4,7 @@ import androidx.compose.material3.SnackbarHostState
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler
@ -23,12 +24,14 @@ internal object UiDependenciesModule {
fun provideUiDependencies(
vibratorHapticManager: VibratorHapticManager,
appThemeModeHolder: AppThemeModeHolder,
designFeatureToggles: DesignFeatureToggles,
): UiDependencies {
return object : UiDependencies {
override val vibratorHapticManager = vibratorHapticManager
override val appThemeModeHolder = appThemeModeHolder
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
override val designFeatureToggles: DesignFeatureToggles = designFeatureToggles
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.di.core.ui
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.tap.core.ui.DefaultDesignFeatureToggles
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface CoreUiBindsModule {
@Binds
fun bindDesignFeatureToggles(impl: DefaultDesignFeatureToggles): DesignFeatureToggles
}

View file

@ -16,7 +16,7 @@
</style>
<style name="SplashTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/background_primary</item>
<item name="windowSplashScreenBackground">@color/background_secondary</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/inset_splash</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>

View file

@ -67,6 +67,10 @@
"name": "FEED_ENABLED",
"version": "undefined"
},
{
"name": "APP_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "GASLESS_TRANSACTIONS_ENABLED",
"version": "5.33.0"

View file

@ -61,6 +61,16 @@ dependencies {
api(deps.jodatime)
implementation(deps.timber)
implementation(deps.markdown)
implementation(deps.haze) {
exclude(module = "activity-compose")
exclude(module = "activity")
exclude(module = "activity-ktx")
}
implementation(deps.haze.materials) {
exclude(module = "activity-compose")
exclude(module = "activity")
exclude(module = "activity-ktx")
}
/** Tests */
testImplementation(deps.test.junit)

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui
interface DesignFeatureToggles {
val isRedesignEnabled: Boolean
}

View file

@ -16,4 +16,6 @@ interface UiDependencies {
val globalSnackbarHostState: SnackbarHostState
val eventMessageHandler: EventMessageHandler
val designFeatureToggles: DesignFeatureToggles
}

View file

@ -0,0 +1,57 @@
package com.tangem.core.ui.components.haze
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.res.LocalRootBackgroundColor
import dev.chrisbanes.haze.*
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
@OptIn(ExperimentalHazeMaterialsApi::class)
@Composable
internal fun ProvideHaze(content: @Composable () -> Unit) {
val hazeState = rememberHazeState()
CompositionLocalProvider(
LocalHazeState provides hazeState,
LocalHazeStyle provides CupertinoMaterials.ultraThin(),
) {
content()
}
}
/**
* Applies a haze effect to the [Modifier] with consideration of global haze settings and power saving mode.
*
* @param configure A lambda to configure the [HazeEffectScope].
* @return A [Modifier] with the configured haze effect applied.
*/
@Composable
fun Modifier.hazeEffectTangem(
style: HazeStyle = HazeStyle.Unspecified,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
val hazeState = LocalHazeState.current
val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value
val rootBackground by LocalRootBackgroundColor.current
return hazeEffect(hazeState, style) {
fallbackTint = HazeTint(rootBackground)
if (isGlobalBlurEnabled) {
configure()
}
blurEnabled = isGlobalBlurEnabled
}
}
/**
* Applies a haze source to the [Modifier] using the current global haze state.
*/
@Composable
fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) =
this.hazeSource(LocalHazeState.current, zIndex, key)

View file

@ -0,0 +1,66 @@
package com.tangem.core.ui.components.powersaving
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.PowerManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class PowerSavingState(
appContext: Context,
lifecycle: androidx.lifecycle.Lifecycle,
) {
private val powerManager =
appContext.getSystemService(Context.POWER_SERVICE) as? PowerManager ?: error("PowerManager not available")
val isPowerSavingModeEnabled: StateFlow<Boolean>
field = MutableStateFlow(powerManager.isPowerSaveMode)
init {
val intentFilter = IntentFilter().apply {
addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
}
val receiver = PowerSavingModeReceiver()
ContextCompat.registerReceiver(
appContext,
receiver,
intentFilter,
ContextCompat.RECEIVER_NOT_EXPORTED,
)
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
appContext.unregisterReceiver(receiver)
}
})
}
inner class PowerSavingModeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) {
isPowerSavingModeEnabled.value = powerManager.isPowerSaveMode
}
}
}
}
@Composable
internal fun rememberPowerSavingState(): PowerSavingState {
val appContext = LocalContext.current.applicationContext
val lifecycle = LocalLifecycleOwner.current.lifecycle
return remember(appContext, lifecycle) {
PowerSavingState(appContext = appContext, lifecycle = lifecycle)
}
}

View file

@ -18,6 +18,8 @@ import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.LocalSystemBarsIconsController
import com.tangem.core.ui.components.SystemBarsIconsController
import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.components.powersaving.PowerSavingState
import com.tangem.core.ui.components.powersaving.rememberPowerSavingState
import com.tangem.core.ui.components.text.BladeAnimation
import com.tangem.core.ui.components.text.rememberBladeAnimation
import com.tangem.core.ui.haptic.DefaultHapticManager
@ -28,6 +30,7 @@ import com.tangem.core.ui.windowsize.WindowSize
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.domain.apptheme.model.AppThemeMode
import com.valentinilk.shimmer.Shimmer
import dev.chrisbanes.haze.HazeState
@OptIn(ExperimentalFoundationApi::class)
@Composable
@ -54,7 +57,13 @@ fun TangemTheme(
overrideSystemBarColors = overrideSystemBarColors,
typography = typography,
dimens = dimens,
content = content,
content = {
if (uiDependencies.designFeatureToggles.isRedesignEnabled) {
TangemThemeRedesign { content() }
} else {
content()
}
},
)
}
@ -114,6 +123,7 @@ fun TangemTheme(
LocalWindowSize provides windowSize,
LocalBladeAnimation provides rememberBladeAnimation(),
LocalSystemBarsIconsController provides systemBarsIconsController,
LocalPowerSavingState provides rememberPowerSavingState(),
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
@ -403,6 +413,18 @@ val LocalBladeAnimation = staticCompositionLocalOf<BladeAnimation> {
error("No MainBottomSheetColor provided")
}
val LocalHazeState = staticCompositionLocalOf<HazeState> {
error("No HazeState provided")
}
val LocalRedesignEnabled = staticCompositionLocalOf<Boolean> {
false
}
val LocalPowerSavingState = compositionLocalOf<PowerSavingState> {
error("No PowerSavingState provided")
}
/**
* Determines whether the dark theme should be used based on the given [AppThemeMode].
*

View file

@ -2,8 +2,12 @@
package com.tangem.core.ui.res
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.haze.ProvideHaze
import com.tangem.core.ui.components.haze.hazeSourceTangem
/**
* Provides additional theming for redesigned components.
@ -21,12 +25,17 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) {
colorScheme = tangemColorScheme(colors = themeColors),
) {
CompositionLocalProvider(
LocalRedesignEnabled provides true,
LocalTangemColors provides themeColors,
LocalTangemColors2 provides if (LocalIsInDarkTheme.current) darkThemeColors2() else lightThemeColors2(),
LocalTangemTypography2 provides TangemTypography2(InterFamily),
LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) },
) {
content()
ProvideHaze {
Box(Modifier.hazeSourceTangem(zIndex = 1f)) {
content()
}
}
}
}
}

View file

@ -12,7 +12,7 @@ internal interface TangemPayCardDetailsBlockComponent {
val state: StateFlow<TangemPayCardDetailsUM>
@Composable
fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier)
fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier)
data class Params(val params: TangemPayDetailsContainerComponent.Params)
}

View file

@ -98,6 +98,7 @@ markdown = "0.7.2"
markdownComposeView = "0.5.4"
usedesk = "4.4.0"
sumsub = "1.38.0"
haze = "1.7.1"
# endregion Other libraries
# region Tools
@ -302,4 +303,6 @@ markdown-composeview = { module = "com.github.jeziellago:compose-markdown", vers
usedesk-chat-sdk = { module = "com.github.Usedesk.Android_SDK:chat-sdk", version.ref = "usedesk" }
usedesk-chat-gui = { module = "com.github.Usedesk.Android_SDK:chat-gui", version.ref = "usedesk" }
sumsub-sdk = { module = "com.sumsub.sns:idensic-mobile-sdk", version.ref = "sumsub" }
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
# endregion Other