Updated on 2026-08-14
This commit is contained in:
parent
c509046a8b
commit
adb8c50ae2
13 changed files with 448 additions and 1 deletions
|
|
@ -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?)
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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"}"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ dependencies {
|
|||
|
||||
/** Domain */
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.settings)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
|
|||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.domain.feedback.repository.FeedbackRepository
|
||||
import com.tangem.domain.settings.UsedeskTokenTtlManager
|
||||
import com.tangem.feature.usedesk.analytics.UsedeskAnalyticsEvents
|
||||
import com.tangem.feature.usedesk.api.UsedeskComponent
|
||||
import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration
|
||||
|
|
@ -29,6 +30,7 @@ internal class UsedeskModel @Inject constructor(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val feedbackRepository: FeedbackRepository,
|
||||
private val usedeskTokenTtlManager: UsedeskTokenTtlManager,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -54,6 +56,7 @@ internal class UsedeskModel @Inject constructor(
|
|||
clientEmail = params.userWalletId,
|
||||
clientInitMessage = params.prefilledMessage,
|
||||
additionalFields = additionalFieldsFor(params.source),
|
||||
tokenTtlMillis = usedeskTokenTtlManager.getTokenTtlMillisSync(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue