Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-01 22:26:21 +05:00
parent c509046a8b
commit adb8c50ae2
13 changed files with 448 additions and 1 deletions

View file

@ -1,11 +1,13 @@
package com.tangem.feature.tester.presentation.actions
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import java.io.File
internal data class TesterActionsContentState(
val hideAllCurrenciesUM: HideAllCurrenciesUM,
val toggleHotWalletRestrictionUM: ToggleHotWalletRestrictionUM,
val usedeskTokenTtlUM: UsedeskTokenTtlUM,
val shareLogsUM: ShareLogsUM,
val onBackClick: () -> Unit,
) {
@ -21,6 +23,24 @@ internal data class TesterActionsContentState(
val onClick: () -> Unit,
)
/**
* State of the Usedesk chat token TTL setting.
*
* @param currentLabel human-readable current TTL (e.g. "7 days", "15 minutes")
* @param presets selectable preset durations
* @param onPresetSelected applies one of the [presets] (value in milliseconds)
* @param onCustomMinutesSelected applies a custom TTL entered in minutes
*/
@Immutable
data class UsedeskTokenTtlUM(
val currentLabel: String,
val presets: ImmutableList<Preset>,
val onPresetSelected: (Long) -> Unit,
val onCustomMinutesSelected: (Long) -> Unit,
) {
data class Preset(val label: String, val millis: Long)
}
@Immutable
data class ShareLogsUM(val file: File?)
}

View file

@ -8,15 +8,23 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.core.ui.components.AdditionalTextInputDialogUM
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.components.TextInputDialog
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -25,12 +33,19 @@ import com.tangem.core.ui.utils.findActivity
import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.toImmutableList
import java.io.File
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Modifier = Modifier) {
var shouldShowTtlSelector by remember { mutableStateOf(value = false) }
var shouldShowTtlCustomInput by remember { mutableStateOf(value = false) }
LazyColumn(
modifier = modifier
.fillMaxSize()
@ -65,6 +80,13 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod
)
}
item {
TesterActionItem(
name = stringResourceSafe(id = R.string.usedesk_token_ttl, state.usedeskTokenTtlUM.currentLabel),
onClick = { shouldShowTtlSelector = true },
)
}
item {
val activity = LocalContext.current.findActivity()
@ -75,6 +97,24 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod
)
}
}
if (shouldShowTtlSelector) {
UsedeskTtlSelectorDialog(
um = state.usedeskTokenTtlUM,
onCustomRequest = {
shouldShowTtlSelector = false
shouldShowTtlCustomInput = true
},
onDismiss = { shouldShowTtlSelector = false },
)
}
if (shouldShowTtlCustomInput) {
UsedeskTtlCustomInputDialog(
um = state.usedeskTokenTtlUM,
onDismiss = { shouldShowTtlCustomInput = false },
)
}
}
@Composable
@ -90,6 +130,62 @@ private fun TesterActionItem(name: String, onClick: () -> Unit, progress: Boolea
)
}
@Composable
private fun UsedeskTtlSelectorDialog(um: UsedeskTokenTtlUM, onCustomRequest: () -> Unit, onDismiss: () -> Unit) {
val customLabel = stringResourceSafe(R.string.usedesk_token_ttl_custom)
val items = remember(um.presets, customLabel) {
(um.presets.map(UsedeskTokenTtlUM.Preset::label) + customLabel).toImmutableList()
}
val selectedIndex = remember(um.presets, um.currentLabel) {
um.presets.indexOfFirst { it.label == um.currentLabel }
.takeIf { it >= 0 }
?: um.presets.size
}
SelectorDialog(
title = stringResourceSafe(R.string.usedesk_token_ttl_title),
items = items,
selectedItemIndex = selectedIndex,
confirmButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_close), onClick = onDismiss),
onSelect = { index ->
val preset = um.presets.getOrNull(index)
if (preset != null) {
um.onPresetSelected(preset.millis)
onDismiss()
} else {
onCustomRequest()
}
},
onDismissDialog = onDismiss,
)
}
@Composable
private fun UsedeskTtlCustomInputDialog(um: UsedeskTokenTtlUM, onDismiss: () -> Unit) {
var value by remember { mutableStateOf(TextFieldValue()) }
val minutes = value.text.toLongOrNull()
val isValid = minutes != null && minutes > 0
TextInputDialog(
title = stringResourceSafe(R.string.usedesk_token_ttl_title),
fieldValue = value,
onValueChange = { newValue -> value = newValue.copy(text = newValue.text.filter(Char::isDigit)) },
textFieldParams = AdditionalTextInputDialogUM(
label = stringResourceSafe(R.string.usedesk_token_ttl_custom),
placeholder = "15",
),
confirmButton = DialogButtonUM(
isEnabled = isValid,
onClick = {
if (isValid) um.onCustomMinutesSelected(minutes)
onDismiss()
},
),
dismissButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_close), onClick = onDismiss),
onDismissDialog = onDismiss,
)
}
private fun Activity.shareFile(file: File?) {
val originalIntent = createEmailShareIntent(activity = this, file = file)
@ -126,6 +222,15 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) {
state = TesterActionsContentState(
hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {},
toggleHotWalletRestrictionUM = ToggleHotWalletRestrictionUM(isEnabled = true) {},
usedeskTokenTtlUM = UsedeskTokenTtlUM(
currentLabel = "7 days",
presets = listOf(
UsedeskTokenTtlUM.Preset(label = "7 days", millis = 7.days.inWholeMilliseconds),
UsedeskTokenTtlUM.Preset(label = "15 minutes", millis = 15.minutes.inWholeMilliseconds),
).toImmutableList(),
onPresetSelected = {},
onCustomMinutesSelected = {},
),
shareLogsUM = TesterActionsContentState.ShareLogsUM(file = null),
onBackClick = {},
),

View file

@ -9,14 +9,21 @@ import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.settings.HotWalletRestrictionManager
import com.tangem.domain.settings.UsedeskTokenTtlManager
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM
import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
@HiltViewModel
internal class TesterActionsViewModel @Inject constructor(
@ -24,6 +31,7 @@ internal class TesterActionsViewModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val walletAccountsSaver: WalletAccountsSaver,
private val hotWalletRestrictionManager: HotWalletRestrictionManager,
private val usedeskTokenTtlManager: UsedeskTokenTtlManager,
) : ViewModel() {
var uiState: TesterActionsContentState by mutableStateOf(initialState)
@ -36,12 +44,19 @@ internal class TesterActionsViewModel @Inject constructor(
isEnabled = hotWalletRestrictionManager.isCreationEnabledSync(),
onClick = this::toggleHotWalletRestriction,
),
usedeskTokenTtlUM = UsedeskTokenTtlUM(
currentLabel = formatTtl(usedeskTokenTtlManager.getTokenTtlMillisSync()),
presets = TTL_PRESETS,
onPresetSelected = this::setUsedeskTokenTtl,
onCustomMinutesSelected = { minutes -> setUsedeskTokenTtl(minutes.minutes.inWholeMilliseconds) },
),
shareLogsUM = TesterActionsContentState.ShareLogsUM(file = feedbackRepository.getLogFile()),
onBackClick = { /* no-op */ },
)
init {
bootstrapHotWalletRestrictionUpdates()
bootstrapUsedeskTokenTtlUpdates()
}
fun setupNavigation(router: InnerTesterRouter) {
@ -76,6 +91,10 @@ internal class TesterActionsViewModel @Inject constructor(
hotWalletRestrictionManager.toggleCreationEnabled()
}
private fun setUsedeskTokenTtl(millis: Long) = viewModelScope.launch {
usedeskTokenTtlManager.setTokenTtlMillis(millis)
}
private fun bootstrapHotWalletRestrictionUpdates() {
hotWalletRestrictionManager.isCreationEnabled()
.onEach { isEnabled ->
@ -87,4 +106,52 @@ internal class TesterActionsViewModel @Inject constructor(
}
.launchIn(viewModelScope)
}
private fun bootstrapUsedeskTokenTtlUpdates() {
usedeskTokenTtlManager.getTokenTtlMillis()
.onEach { millis ->
uiState = uiState.copy(
usedeskTokenTtlUM = uiState.usedeskTokenTtlUM.copy(
currentLabel = formatTtl(millis),
),
)
}
.launchIn(viewModelScope)
}
private companion object {
val TTL_PRESETS = persistentListOf(
UsedeskTokenTtlUM.Preset(
label = formatTtl(7.days.inWholeMilliseconds),
millis = 7.days.inWholeMilliseconds,
),
UsedeskTokenTtlUM.Preset(
label = formatTtl(1.days.inWholeMilliseconds),
millis = 1.days.inWholeMilliseconds,
),
UsedeskTokenTtlUM.Preset(
label = formatTtl(1.hours.inWholeMilliseconds),
millis = 1.hours.inWholeMilliseconds,
),
UsedeskTokenTtlUM.Preset(
label = formatTtl(15.minutes.inWholeMilliseconds),
millis = 15.minutes.inWholeMilliseconds,
),
)
/** Picks the largest whole unit (days → hours → minutes → seconds) for a readable label. */
fun formatTtl(millis: Long): String {
val duration = millis.milliseconds
return when {
duration.inWholeDays >= 1 && duration == duration.inWholeDays.days -> duration.inWholeDays.unit("day")
duration.inWholeHours >= 1 && duration == duration.inWholeHours.hours ->
duration.inWholeHours.unit("hour")
duration.inWholeMinutes >= 1 -> duration.inWholeMinutes.unit("minute")
else -> duration.inWholeSeconds.unit("second")
}
}
fun Long.unit(name: String): String = "$this $name${if (this == 1L) "" else "s"}"
}
}

View file

@ -15,6 +15,9 @@
<string name="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
<string name="blockchain_providers" translatable="false">Blockchain providers</string>
<string name="toggle_hot_wallet_restriction" translatable="false">Hot wallet creation restriction - %s</string>
<string name="usedesk_token_ttl" translatable="false">Usedesk chat token TTL - %s</string>
<string name="usedesk_token_ttl_title" translatable="false">Usedesk chat token TTL</string>
<string name="usedesk_token_ttl_custom" translatable="false">Custom (minutes)</string>
<string name="share_logs" translatable="false">Share logs</string>
<string name="test_push" translatable="false">Test push</string>
<string name="accounts" translatable="false">Accounts</string>