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

@ -0,0 +1,50 @@
package com.tangem.data.settings
import androidx.datastore.preferences.core.longPreferencesKey
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.domain.settings.UsedeskTokenTtlManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
/**
* Development implementation of [UsedeskTokenTtlManager].
*
* Reads and writes the Usedesk chat token TTL from [AppPreferencesStore],
* allowing testers to override it via the Tester Menu.
* The preference [kotlinx.coroutines.flow.Flow] is converted to a [StateFlow] on construction,
* so [getTokenTtlMillisSync] can be called from non-suspending contexts.
* Defaults to [UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS] when no value is stored.
*/
internal class DevUsedeskTokenTtlManager(
private val appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
) : UsedeskTokenTtlManager {
private val ttlMillisState: StateFlow<Long> =
appPreferencesStore
.get(key = USEDESK_TOKEN_TTL_MILLIS_KEY, default = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS)
.stateIn(
scope = CoroutineScope(dispatchers.io + SupervisorJob()),
started = SharingStarted.Eagerly,
initialValue = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS,
)
override fun getTokenTtlMillis(): StateFlow<Long> = ttlMillisState
override fun getTokenTtlMillisSync(): Long = ttlMillisState.value
override suspend fun setTokenTtlMillis(millis: Long) {
appPreferencesStore.editData { preferences ->
preferences[USEDESK_TOKEN_TTL_MILLIS_KEY] = millis
}
}
private companion object {
val USEDESK_TOKEN_TTL_MILLIS_KEY = longPreferencesKey(name = "usedeskTokenTtlMillis")
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.settings
import com.tangem.domain.settings.UsedeskTokenTtlManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Production implementation of [UsedeskTokenTtlManager].
*
* The Usedesk chat token TTL is fixed to [UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS] in production
* a tester override must never leak into release builds.
* [setTokenTtlMillis] is a no-op since the TTL cannot be changed at runtime.
*/
internal class ProdUsedeskTokenTtlManager : UsedeskTokenTtlManager {
private val state: StateFlow<Long> = MutableStateFlow(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS)
override fun getTokenTtlMillis(): StateFlow<Long> = state
override fun getTokenTtlMillisSync(): Long = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS
override suspend fun setTokenTtlMillis(millis: Long) = Unit
}

View file

@ -6,11 +6,14 @@ import com.tangem.data.settings.DefaultAppRatingRepository
import com.tangem.data.settings.DefaultPermissionRepository import com.tangem.data.settings.DefaultPermissionRepository
import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.data.settings.DefaultSettingsRepository
import com.tangem.data.settings.DevHotWalletRestrictionManager import com.tangem.data.settings.DevHotWalletRestrictionManager
import com.tangem.data.settings.DevUsedeskTokenTtlManager
import com.tangem.data.settings.ProdHotWalletRestrictionManager import com.tangem.data.settings.ProdHotWalletRestrictionManager
import com.tangem.data.settings.ProdUsedeskTokenTtlManager
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.settings.HotWalletRestrictionManager import com.tangem.domain.settings.HotWalletRestrictionManager
import com.tangem.domain.settings.UsedeskTokenTtlManager
import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.settings.repositories.PermissionRepository
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository
@ -72,4 +75,17 @@ internal object SettingsDataModule {
ProdHotWalletRestrictionManager() ProdHotWalletRestrictionManager()
} }
} }
@Provides
@Singleton
fun provideUsedeskTokenTtlManager(
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): UsedeskTokenTtlManager {
return if (BuildConfig.TESTER_MENU_ENABLED) {
DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers)
} else {
ProdUsedeskTokenTtlManager()
}
}
} }

View file

@ -0,0 +1,104 @@
package com.tangem.data.settings
import androidx.datastore.preferences.core.Preferences
import com.google.common.truth.Truth.assertThat
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.domain.settings.UsedeskTokenTtlManager
import com.tangem.test.core.getEmittedValues
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DevUsedeskTokenTtlManagerTest {
private val appPreferencesStore = mockk<AppPreferencesStore>(relaxed = true)
private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider()
@BeforeEach
fun setup() {
mockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
}
@AfterEach
fun tearDown() {
unmockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
}
@Test
fun `GIVEN stored ttl WHEN getTokenTtlMillis THEN emits stored value`() = runTest {
// Arrange
val storedTtl = 15L * 60 * 1000
every {
appPreferencesStore.get(key = any<Preferences.Key<Long>>(), default = DEFAULT)
} returns flowOf(storedTtl)
val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers)
// Act
val emitted = getEmittedValues(manager.getTokenTtlMillis())
// Assert
assertThat(emitted).containsExactly(storedTtl)
}
@Test
fun `GIVEN no stored ttl WHEN getTokenTtlMillisSync THEN returns default`() = runTest {
// Arrange
every {
appPreferencesStore.get(key = any<Preferences.Key<Long>>(), default = DEFAULT)
} returns flowOf(DEFAULT)
val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers)
// Act & Assert
assertThat(manager.getTokenTtlMillisSync()).isEqualTo(DEFAULT)
}
@Test
fun `GIVEN stored ttl WHEN getTokenTtlMillisSync THEN returns cached value`() = runTest {
// Arrange
val storedTtl = 60L * 60 * 1000
every {
appPreferencesStore.get(key = any<Preferences.Key<Long>>(), default = DEFAULT)
} returns flowOf(storedTtl)
val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers)
// Act & Assert
assertThat(manager.getTokenTtlMillisSync()).isEqualTo(storedTtl)
}
@Test
fun `WHEN setTokenTtlMillis THEN opens editData transaction`() = runTest {
// Arrange
every {
appPreferencesStore.get(key = any<Preferences.Key<Long>>(), default = DEFAULT)
} returns flowOf(DEFAULT)
coEvery { appPreferencesStore.editData(any()) } returns mockk(relaxed = true)
val manager = DevUsedeskTokenTtlManager(appPreferencesStore, dispatchers)
// Act
manager.setTokenTtlMillis(millis = 15L * 60 * 1000)
// Assert
coVerify { appPreferencesStore.editData(any()) }
}
private companion object {
val DEFAULT = UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.settings
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.settings.UsedeskTokenTtlManager
import com.tangem.test.core.getEmittedValues
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ProdUsedeskTokenTtlManagerTest {
private val manager = ProdUsedeskTokenTtlManager()
@Test
fun `WHEN getTokenTtlMillis THEN always emits default`() = runTest {
val emitted = getEmittedValues(manager.getTokenTtlMillis())
assertThat(emitted).containsExactly(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS)
}
@Test
fun `WHEN getTokenTtlMillisSync THEN returns default`() = runTest {
assertThat(manager.getTokenTtlMillisSync()).isEqualTo(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS)
}
@Test
fun `GIVEN override attempt WHEN setTokenTtlMillis THEN sync still returns default`() = runTest {
manager.setTokenTtlMillis(millis = 15L * 60 * 1000)
assertThat(manager.getTokenTtlMillisSync()).isEqualTo(UsedeskTokenTtlManager.DEFAULT_TTL_MILLIS)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.settings
import kotlinx.coroutines.flow.StateFlow
import kotlin.time.Duration.Companion.days
/**
* Manages the lifetime (TTL) of the Usedesk support-chat token.
*
* The token is reused while the time since the last chat interaction is below this TTL;
* once it expires the SDK obtains a fresh token on the next chat init.
*
* In production the TTL is fixed to [DEFAULT_TTL_MILLIS]. Debug builds let testers override it
* via the Tester Menu (e.g. shorten it to a few minutes to verify token-refresh behaviour).
*/
interface UsedeskTokenTtlManager {
fun getTokenTtlMillis(): StateFlow<Long>
fun getTokenTtlMillisSync(): Long
suspend fun setTokenTtlMillis(millis: Long)
companion object {
val DEFAULT_TTL_MILLIS: Long = 7.days.inWholeMilliseconds
}
}

View file

@ -1,11 +1,13 @@
package com.tangem.feature.tester.presentation.actions package com.tangem.feature.tester.presentation.actions
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
import java.io.File import java.io.File
internal data class TesterActionsContentState( internal data class TesterActionsContentState(
val hideAllCurrenciesUM: HideAllCurrenciesUM, val hideAllCurrenciesUM: HideAllCurrenciesUM,
val toggleHotWalletRestrictionUM: ToggleHotWalletRestrictionUM, val toggleHotWalletRestrictionUM: ToggleHotWalletRestrictionUM,
val usedeskTokenTtlUM: UsedeskTokenTtlUM,
val shareLogsUM: ShareLogsUM, val shareLogsUM: ShareLogsUM,
val onBackClick: () -> Unit, val onBackClick: () -> Unit,
) { ) {
@ -21,6 +23,24 @@ internal data class TesterActionsContentState(
val onClick: () -> Unit, 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 @Immutable
data class ShareLogsUM(val file: File?) 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider 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.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.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme 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.impl.R
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM 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.ToggleHotWalletRestrictionUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.UsedeskTokenTtlUM
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.toImmutableList
import java.io.File import java.io.File
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.minutes
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Modifier = Modifier) { internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Modifier = Modifier) {
var shouldShowTtlSelector by remember { mutableStateOf(value = false) }
var shouldShowTtlCustomInput by remember { mutableStateOf(value = false) }
LazyColumn( LazyColumn(
modifier = modifier modifier = modifier
.fillMaxSize() .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 { item {
val activity = LocalContext.current.findActivity() 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 @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?) { private fun Activity.shareFile(file: File?) {
val originalIntent = createEmailShareIntent(activity = this, file = file) val originalIntent = createEmailShareIntent(activity = this, file = file)
@ -126,6 +222,15 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) {
state = TesterActionsContentState( state = TesterActionsContentState(
hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {}, hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {},
toggleHotWalletRestrictionUM = ToggleHotWalletRestrictionUM(isEnabled = true) {}, 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), shareLogsUM = TesterActionsContentState.ShareLogsUM(file = null),
onBackClick = {}, 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.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.settings.HotWalletRestrictionManager 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.HideAllCurrenciesUM
import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleHotWalletRestrictionUM 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 com.tangem.feature.tester.presentation.navigation.InnerTesterRouter
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject 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 @HiltViewModel
internal class TesterActionsViewModel @Inject constructor( internal class TesterActionsViewModel @Inject constructor(
@ -24,6 +31,7 @@ internal class TesterActionsViewModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository, private val userWalletsListRepository: UserWalletsListRepository,
private val walletAccountsSaver: WalletAccountsSaver, private val walletAccountsSaver: WalletAccountsSaver,
private val hotWalletRestrictionManager: HotWalletRestrictionManager, private val hotWalletRestrictionManager: HotWalletRestrictionManager,
private val usedeskTokenTtlManager: UsedeskTokenTtlManager,
) : ViewModel() { ) : ViewModel() {
var uiState: TesterActionsContentState by mutableStateOf(initialState) var uiState: TesterActionsContentState by mutableStateOf(initialState)
@ -36,12 +44,19 @@ internal class TesterActionsViewModel @Inject constructor(
isEnabled = hotWalletRestrictionManager.isCreationEnabledSync(), isEnabled = hotWalletRestrictionManager.isCreationEnabledSync(),
onClick = this::toggleHotWalletRestriction, 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()), shareLogsUM = TesterActionsContentState.ShareLogsUM(file = feedbackRepository.getLogFile()),
onBackClick = { /* no-op */ }, onBackClick = { /* no-op */ },
) )
init { init {
bootstrapHotWalletRestrictionUpdates() bootstrapHotWalletRestrictionUpdates()
bootstrapUsedeskTokenTtlUpdates()
} }
fun setupNavigation(router: InnerTesterRouter) { fun setupNavigation(router: InnerTesterRouter) {
@ -76,6 +91,10 @@ internal class TesterActionsViewModel @Inject constructor(
hotWalletRestrictionManager.toggleCreationEnabled() hotWalletRestrictionManager.toggleCreationEnabled()
} }
private fun setUsedeskTokenTtl(millis: Long) = viewModelScope.launch {
usedeskTokenTtlManager.setTokenTtlMillis(millis)
}
private fun bootstrapHotWalletRestrictionUpdates() { private fun bootstrapHotWalletRestrictionUpdates() {
hotWalletRestrictionManager.isCreationEnabled() hotWalletRestrictionManager.isCreationEnabled()
.onEach { isEnabled -> .onEach { isEnabled ->
@ -87,4 +106,52 @@ internal class TesterActionsViewModel @Inject constructor(
} }
.launchIn(viewModelScope) .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="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
<string name="blockchain_providers" translatable="false">Blockchain providers</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="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="share_logs" translatable="false">Share logs</string>
<string name="test_push" translatable="false">Test push</string> <string name="test_push" translatable="false">Test push</string>
<string name="accounts" translatable="false">Accounts</string> <string name="accounts" translatable="false">Accounts</string>

View file

@ -26,6 +26,7 @@ dependencies {
/** Domain */ /** Domain */
implementation(projects.domain.feedback) implementation(projects.domain.feedback)
implementation(projects.domain.settings)
/** DI */ /** DI */
implementation(deps.hilt.android) implementation(deps.hilt.android)

View file

@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.domain.feedback.repository.FeedbackRepository 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.analytics.UsedeskAnalyticsEvents
import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.usedesk.api.UsedeskComponent
import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration import com.tangem.usedesk.chat_sdk.entity.UsedeskChatConfiguration
@ -29,6 +30,7 @@ internal class UsedeskModel @Inject constructor(
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsEventHandler: AnalyticsEventHandler,
private val feedbackRepository: FeedbackRepository, private val feedbackRepository: FeedbackRepository,
private val usedeskTokenTtlManager: UsedeskTokenTtlManager,
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
) : Model() { ) : Model() {
@ -54,6 +56,7 @@ internal class UsedeskModel @Inject constructor(
clientEmail = params.userWalletId, clientEmail = params.userWalletId,
clientInitMessage = params.prefilledMessage, clientInitMessage = params.prefilledMessage,
additionalFields = additionalFieldsFor(params.source), additionalFields = additionalFieldsFor(params.source),
tokenTtlMillis = usedeskTokenTtlManager.getTokenTtlMillisSync(),
), ),
) )
} }

View file

@ -13,7 +13,7 @@ tangemVico = "tangem-master-21"
#tangemVico = "0.0.1" # Keep it! - used for local builds ^ #tangemVico = "0.0.1" # Keep it! - used for local builds ^
tangemHotSdk = "develop-550" tangemHotSdk = "develop-550"
#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^
tangemUsedeskSdk = "main-9" tangemUsedeskSdk = "main-10"
#tangemUsedeskSdk = "0.0.1" # Keep it! - used for local builds ^ #tangemUsedeskSdk = "0.0.1" # Keep it! - used for local builds ^
[libraries] [libraries]